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,689 @@
1
+ // @ts-check
2
+
3
+ import Parser from "tree-sitter"
4
+ import PythonLanguage from "tree-sitter-python"
5
+ import {missingType, parseFailure, SemantifoldDiagnostic, unsupportedSyntax} from "../diagnostic.js"
6
+ import {locationFromOffsets, moduleLocation, utf8ByteOffsetToUtf16Offset} from "../semantic/location.js"
7
+ import {withAdaptedOperation} from "../semantic/operators.js"
8
+ import {withParserRanges} from "../semantic/provenance.js"
9
+ import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
10
+ import {requireSourceScalarType} from "./scalars.js"
11
+
12
+ const parser = new Parser()
13
+
14
+ parser.setLanguage(/** @type {import("tree-sitter").Language} */ (/** @type {unknown} */ (PythonLanguage)))
15
+
16
+ const immutableMarker = "# @semantifold-immutable"
17
+ const pythonReservedIdentifiers = new Set([
18
+ "False", "None", "True", "and", "as", "assert", "async", "await", "break", "case", "class", "continue",
19
+ "def", "del", "elif", "else", "except", "exec", "finally", "for", "from", "global", "if", "import", "in",
20
+ "is", "lambda", "match", "nonlocal", "not", "or", "pass", "print", "raise", "return", "try", "type",
21
+ "while", "with", "yield", "_", "__debug__", "int", "bool", "str"
22
+ ])
23
+ const binaryOperations = new Map([
24
+ ["+", "Add"],
25
+ ["-", "Subtract"],
26
+ ["*", "Multiply"]
27
+ ])
28
+ const comparisonOperations = new Map([
29
+ ["==", "Equal"],
30
+ ["!=", "NotEqual"],
31
+ ["<", "LessThan"],
32
+ ["<=", "LessThanOrEqual"],
33
+ [">", "GreaterThan"],
34
+ [">=", "GreaterThanOrEqual"]
35
+ ])
36
+
37
+ /** @typedef {{comments: import("tree-sitter").SyntaxNode[], usedImmutableMarkers: Set<import("tree-sitter").SyntaxNode>}} PythonContext */
38
+
39
+ /**
40
+ * Routes the binding's UTF-16 index through the shared UTF-8 byte converter.
41
+ * node-tree-sitter parses JavaScript strings as UTF-16LE and exposes code-unit
42
+ * indexes even though the grammar's native coordinate contract is byte-based.
43
+ * @param {string} source - Complete source.
44
+ * @param {number} index - Binding UTF-16 index.
45
+ * @returns {number} Verified UTF-16 offset.
46
+ */
47
+ function bindingIndexToUtf16Offset(source, index) {
48
+ if (!Number.isInteger(index) || index < 0 || index > source.length ||
49
+ index > 0 && index < source.length && /[\uD800-\uDBFF]/u.test(source[index - 1]) && /[\uDC00-\uDFFF]/u.test(source[index])) {
50
+ throw new RangeError(`Invalid Tree-sitter source index: ${index}`)
51
+ }
52
+ const byteOffset = new TextEncoder().encode(source.slice(0, index)).length
53
+
54
+ return utf8ByteOffsetToUtf16Offset(source, byteOffset)
55
+ }
56
+
57
+ /**
58
+ * Finds the first unpaired UTF-16 surrogate.
59
+ * @param {string} source - Complete source.
60
+ * @returns {number} Invalid offset, or -1.
61
+ */
62
+ function firstLoneSurrogateOffset(source) {
63
+ for (let index = 0; index < source.length; index += 1) {
64
+ const unit = source.charCodeAt(index)
65
+
66
+ if (unit >= 0xD800 && unit <= 0xDBFF) {
67
+ const next = source.charCodeAt(index + 1)
68
+
69
+ if (next >= 0xDC00 && next <= 0xDFFF) index += 1
70
+ else return index
71
+ } else if (unit >= 0xDC00 && unit <= 0xDFFF) return index
72
+ }
73
+
74
+ return -1
75
+ }
76
+
77
+ /**
78
+ * Converts a Tree-sitter byte range into Semantifold's UTF-16 coordinates.
79
+ * @param {import("tree-sitter").SyntaxNode} node - Parser node.
80
+ * @param {string} filename - Source filename.
81
+ * @param {string} source - Complete source.
82
+ * @returns {import("../semantic/types.js").SourceLocation} Exact location.
83
+ */
84
+ function nodeLocation(node, filename, source) {
85
+ return locationFromOffsets(
86
+ filename,
87
+ source,
88
+ bindingIndexToUtf16Offset(source, node.startIndex),
89
+ bindingIndexToUtf16Offset(source, node.endIndex)
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Requires one parser field.
95
+ * @param {import("tree-sitter").SyntaxNode} node - Field owner.
96
+ * @param {string} field - Grammar field name.
97
+ * @param {string} filename - Source filename.
98
+ * @param {string} source - Complete source.
99
+ * @returns {import("tree-sitter").SyntaxNode} Field node.
100
+ */
101
+ function requiredField(node, field, filename, source) {
102
+ const child = node.childForFieldName(field)
103
+
104
+ if (!child) return unsupportedSyntax("python", `${node.type} without ${field}`, nodeLocation(node, filename, source))
105
+
106
+ return child
107
+ }
108
+
109
+ /**
110
+ * Validates every named and anonymous parser child and rejects recovery.
111
+ * @param {import("tree-sitter").SyntaxNode} node - Current parser node.
112
+ * @param {string} filename - Source filename.
113
+ * @param {string} source - Complete source.
114
+ * @returns {void}
115
+ */
116
+ function validateParserTree(node, filename, source) {
117
+ if (node.isError || node.isMissing) {
118
+ throw new SemantifoldDiagnostic({
119
+ code: "PARSE_ERROR",
120
+ language: "python",
121
+ location: nodeLocation(node, filename, source),
122
+ message: `Tree-sitter exposed ${node.isMissing ? "a missing" : "an error"} recovery node '${node.type}'.`
123
+ })
124
+ }
125
+
126
+ for (let index = 0; index < node.childCount; index += 1) {
127
+ const child = node.child(index)
128
+
129
+ if (!child) throw new Error(`Tree-sitter omitted child ${index} of ${node.type}.`)
130
+ validateParserTree(child, filename, source)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Rejects Python identifiers that cannot safely participate in the exact profile.
136
+ * @param {import("tree-sitter").SyntaxNode} node - Identifier node.
137
+ * @param {string} role - Diagnostic role.
138
+ * @param {string} filename - Source filename.
139
+ * @param {string} source - Complete source.
140
+ * @returns {string} Accepted name.
141
+ */
142
+ function identifier(node, role, filename, source) {
143
+ const name = node.text
144
+
145
+ if (node.type != "identifier" || pythonReservedIdentifiers.has(name) || name.normalize("NFKC") != name) {
146
+ return unsupportedSyntax("python", `${role} identifier '${name}'`, nodeLocation(node, filename, source))
147
+ }
148
+
149
+ return name
150
+ }
151
+
152
+ /**
153
+ * Converts one exact int/bool/str annotation.
154
+ * @param {import("tree-sitter").SyntaxNode | null} node - Type node, if present.
155
+ * @param {string} subject - Typed subject.
156
+ * @param {import("../semantic/types.js").SourceLocation} ownerLocation - Owning declaration.
157
+ * @param {string} filename - Source filename.
158
+ * @param {string} source - Complete source.
159
+ * @returns {import("../semantic/types.js").TypeReference} Semantic type.
160
+ */
161
+ function convertType(node, subject, ownerLocation, filename, source) {
162
+ if (!node) return missingType("python", subject, ownerLocation)
163
+ const typeLocation = nodeLocation(node, filename, source)
164
+ const children = node.namedChildren.filter((child) => child.type != "comment")
165
+
166
+ if (node.type != "type" || children.length != 1 || children[0].type != "identifier" ||
167
+ !["int", "bool", "str"].includes(children[0].text) || node.text != children[0].text) {
168
+ return unsupportedSyntax("python", "unsupported scalar annotation", typeLocation)
169
+ }
170
+
171
+ return requireSourceScalarType("python", children[0].text, subject, ownerLocation, typeLocation)
172
+ }
173
+
174
+ /**
175
+ * Finds exactly one anonymous operator token directly owned by a parser node.
176
+ * @param {import("tree-sitter").SyntaxNode} node - Operation node.
177
+ * @param {Set<string>} spellings - Accepted spellings.
178
+ * @param {string} filename - Source filename.
179
+ * @param {string} source - Complete source.
180
+ * @returns {import("tree-sitter").SyntaxNode} Operator token.
181
+ */
182
+ function operatorToken(node, spellings, filename, source) {
183
+ const tokens = node.children.filter((child) => !child.isNamed && spellings.has(child.type))
184
+
185
+ if (tokens.length != 1) return unsupportedSyntax("python", `${node.type} operator`, nodeLocation(node, filename, source))
186
+
187
+ return tokens[0]
188
+ }
189
+
190
+ /**
191
+ * Converts one supported Python expression.
192
+ * @param {import("tree-sitter").SyntaxNode} node - Expression node.
193
+ * @param {string} filename - Source filename.
194
+ * @param {string} source - Complete source.
195
+ * @returns {import("../semantic/types.js").Expression} Semantic expression.
196
+ */
197
+ function convertExpression(node, filename, source) {
198
+ const location = nodeLocation(node, filename, source)
199
+
200
+ if (node.type == "parenthesized_expression") {
201
+ const expressions = node.namedChildren.filter((child) => child.type != "comment")
202
+
203
+ if (expressions.length != 1) return unsupportedSyntax("python", node.type, location)
204
+
205
+ return convertExpression(expressions[0], filename, source)
206
+ }
207
+ if (node.type == "identifier") {
208
+ const name = identifier(node, "reference", filename, source)
209
+
210
+ return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name}, {name: location})
211
+ }
212
+ if (node.type == "integer") {
213
+ let value
214
+
215
+ try {
216
+ value = Number(BigInt(node.text.replaceAll("_", "")))
217
+ } catch {
218
+ return unsupportedSyntax("python", "integer literal", location)
219
+ }
220
+ if (!Number.isSafeInteger(value)) return unsupportedSyntax("python", "non-safe integer literal", location)
221
+
222
+ return withParserRanges({kind: /** @type {const} */ ("IntegerLiteral"), location, value}, {literal: location})
223
+ }
224
+ if (node.type == "true" || node.type == "false") {
225
+ return withParserRanges({kind: /** @type {const} */ ("BooleanLiteral"), location, value: node.type == "true"}, {literal: location})
226
+ }
227
+ if (node.type == "string") {
228
+ return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: decodeString(node, filename, source)}, {
229
+ literal: location
230
+ })
231
+ }
232
+ if (node.type == "concatenated_string") return unsupportedSyntax("python", "implicit string concatenation", location)
233
+ if (node.type == "unary_operator") {
234
+ const operandNode = requiredField(node, "argument", filename, source)
235
+ const operator = operatorToken(node, new Set(["-"]), filename, source)
236
+ const semantic = withAdaptedOperation(withParserRanges({
237
+ kind: "UnaryExpression",
238
+ location,
239
+ operand: convertExpression(operandNode, filename, source)
240
+ }, {operator: nodeLocation(operator, filename, source)}), "Negate")
241
+
242
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
243
+ }
244
+ if (node.type == "not_operator") {
245
+ const operandNode = requiredField(node, "argument", filename, source)
246
+ const operator = operatorToken(node, new Set(["not"]), filename, source)
247
+ const semantic = withAdaptedOperation(withParserRanges({
248
+ kind: "UnaryExpression",
249
+ location,
250
+ operand: convertExpression(operandNode, filename, source)
251
+ }, {operator: nodeLocation(operator, filename, source)}), "Not")
252
+
253
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
254
+ }
255
+ if (node.type == "binary_operator" || node.type == "comparison_operator" || node.type == "boolean_operator") {
256
+ const expressionChildren = node.namedChildren.filter((child) => child.type != "comment")
257
+ const leftNode = node.childForFieldName("left") ?? expressionChildren[0]
258
+ const rightNode = node.childForFieldName("right") ?? expressionChildren[1]
259
+
260
+ if (!leftNode || !rightNode) return unsupportedSyntax("python", node.type, location)
261
+ const operations = node.type == "binary_operator" ? binaryOperations : node.type == "comparison_operator"
262
+ ? comparisonOperations : new Map([["and", "And"], ["or", "Or"]])
263
+ const operator = operatorToken(node, new Set(operations.keys()), filename, source)
264
+ const adapted = operations.get(operator.type)
265
+
266
+ if (!adapted || expressionChildren.length != 2) {
267
+ return unsupportedSyntax("python", node.type, location)
268
+ }
269
+ const semantic = withAdaptedOperation(withParserRanges({
270
+ kind: "BinaryExpression",
271
+ left: convertExpression(leftNode, filename, source),
272
+ location,
273
+ right: convertExpression(rightNode, filename, source)
274
+ }, {operator: nodeLocation(operator, filename, source)}), /** @type {import("../semantic/operators.js").AdaptedOperation} */ (adapted))
275
+
276
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
277
+ }
278
+ if (node.type == "call") {
279
+ const calleeNode = requiredField(node, "function", filename, source)
280
+ const argumentsNode = requiredField(node, "arguments", filename, source)
281
+
282
+ if (calleeNode.type != "identifier" || argumentsNode.type != "argument_list") {
283
+ return unsupportedSyntax("python", "dynamic call", location)
284
+ }
285
+ const callee = identifier(calleeNode, "callee", filename, source)
286
+ const arguments_ = argumentsNode.namedChildren.filter((child) => child.type != "comment")
287
+ .map((argument) => convertExpression(argument, filename, source))
288
+
289
+ return withParserRanges({arguments: arguments_, callee, kind: /** @type {const} */ ("CallExpression"), location}, {
290
+ callee: nodeLocation(calleeNode, filename, source)
291
+ })
292
+ }
293
+
294
+ return unsupportedSyntax("python", node.type, location)
295
+ }
296
+
297
+ /**
298
+ * Decodes one non-prefixed, non-triple Python string without evaluating source.
299
+ * @param {import("tree-sitter").SyntaxNode} node - String node.
300
+ * @param {string} filename - Source filename.
301
+ * @param {string} source - Complete source.
302
+ * @returns {string} Unicode scalar string.
303
+ */
304
+ function decodeString(node, filename, source) {
305
+ const location = nodeLocation(node, filename, source)
306
+ const text = node.text
307
+ const quote = text[0]
308
+
309
+ if ((quote != "\"" && quote != "'") || text.at(-1) != quote || text.startsWith(`${quote}${quote}${quote}`)) {
310
+ return unsupportedSyntax("python", "prefixed or triple-quoted string", location)
311
+ }
312
+ let decoded = ""
313
+
314
+ for (let index = 1; index < text.length - 1; index += 1) {
315
+ const character = text[index]
316
+
317
+ if (character != "\\") {
318
+ decoded += character
319
+ continue
320
+ }
321
+ const escape = text[++index]
322
+
323
+ if (escape === undefined) return unsupportedSyntax("python", "invalid string escape", location)
324
+ const simple = new Map([
325
+ ["\\", "\\"], ["'", "'"], ["\"", "\""], ["a", "\u0007"], ["b", "\b"], ["f", "\f"], ["n", "\n"],
326
+ ["r", "\r"], ["t", "\t"], ["v", "\v"]
327
+ ])
328
+
329
+ if (simple.has(escape)) {
330
+ decoded += simple.get(escape)
331
+ continue
332
+ }
333
+ if (/[0-7]/u.test(escape)) {
334
+ let digits = escape
335
+
336
+ while (digits.length < 3 && /[0-7]/u.test(text[index + 1] ?? "")) digits += text[++index]
337
+ decoded += String.fromCodePoint(Number.parseInt(digits, 8))
338
+ continue
339
+ }
340
+ const widths = new Map([["x", 2], ["u", 4], ["U", 8]])
341
+ const width = widths.get(escape)
342
+
343
+ if (!width) return unsupportedSyntax("python", `unsupported string escape \\${escape}`, location)
344
+ const digits = text.slice(index + 1, index + 1 + width)
345
+ if (digits.length != width || !/^[0-9A-Fa-f]+$/u.test(digits)) return unsupportedSyntax("python", "invalid string escape", location)
346
+ index += width
347
+ const codePoint = Number.parseInt(digits, 16)
348
+
349
+ if (codePoint > 0x10FFFF || codePoint >= 0xD800 && codePoint <= 0xDFFF) {
350
+ return unsupportedSyntax("python", "invalid Unicode string escape", location)
351
+ }
352
+ decoded += String.fromCodePoint(codePoint)
353
+ }
354
+
355
+ if (!hasOnlyUnicodeScalars(decoded)) return unsupportedSyntax("python", "invalid Unicode string literal", location)
356
+
357
+ return decoded
358
+ }
359
+
360
+ /**
361
+ * Converts one annotated assignment or plain assignment.
362
+ * @param {import("tree-sitter").SyntaxNode} node - Assignment node.
363
+ * @param {PythonContext} context - Comment association state.
364
+ * @param {Set<string>} visible - Visible bindings in this lexical scope.
365
+ * @param {string} filename - Source filename.
366
+ * @param {string} source - Complete source.
367
+ * @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
368
+ */
369
+ function convertAssignment(node, context, visible, filename, source) {
370
+ const location = nodeLocation(node, filename, source)
371
+ const left = requiredField(node, "left", filename, source)
372
+ const right = requiredField(node, "right", filename, source)
373
+ const typeNode = node.childForFieldName("type")
374
+
375
+ if (left.type != "identifier") return unsupportedSyntax("python", "non-identifier assignment target", nodeLocation(left, filename, source))
376
+ const name = identifier(left, typeNode ? "local" : "assignment target", filename, source)
377
+ const nameLocation = nodeLocation(left, filename, source)
378
+ const operator = operatorToken(node, new Set(["="]), filename, source)
379
+
380
+ if (!typeNode) {
381
+ if (!visible.has(name)) return missingType("python", `Local '${name}'`, location)
382
+ const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: nameLocation, name}, {name: nameLocation})
383
+
384
+ return withParserRanges({
385
+ expression: convertExpression(right, filename, source),
386
+ kind: /** @type {const} */ ("AssignmentStatement"),
387
+ location,
388
+ target
389
+ }, {operator: nodeLocation(operator, filename, source)})
390
+ }
391
+
392
+ const declaration = withParserRanges({
393
+ initializer: convertExpression(right, filename, source),
394
+ kind: /** @type {const} */ ("LocalDeclaration"),
395
+ location,
396
+ mutable: !takeImmutableMarker(node, context),
397
+ name,
398
+ type: convertType(typeNode, `Local '${name}'`, location, filename, source)
399
+ }, {name: nameLocation, operator: nodeLocation(operator, filename, source)})
400
+
401
+ visible.add(name)
402
+
403
+ return declaration
404
+ }
405
+
406
+ /**
407
+ * Associates the exact immediately preceding immutable marker with a local.
408
+ * @param {import("tree-sitter").SyntaxNode} node - Local assignment.
409
+ * @param {PythonContext} context - Comment state.
410
+ * @returns {boolean} Whether the local is immutable.
411
+ */
412
+ function takeImmutableMarker(node, context) {
413
+ const marker = context.comments.find((comment) => comment.text == immutableMarker &&
414
+ comment.endPosition.row + 1 == node.startPosition.row && comment.startPosition.column == node.startPosition.column)
415
+
416
+ if (!marker) return false
417
+ context.usedImmutableMarkers.add(marker)
418
+
419
+ return true
420
+ }
421
+
422
+ /**
423
+ * Converts one statement.
424
+ * @param {import("tree-sitter").SyntaxNode} node - Statement node.
425
+ * @param {PythonContext} context - Comment state.
426
+ * @param {Set<string>} visible - Visible bindings in this lexical scope.
427
+ * @param {string} filename - Source filename.
428
+ * @param {string} source - Complete source.
429
+ * @returns {import("../semantic/types.js").Statement} Semantic statement.
430
+ */
431
+ function convertStatement(node, context, visible, filename, source) {
432
+ const location = nodeLocation(node, filename, source)
433
+
434
+ if (node.type == "return_statement") {
435
+ const expressions = node.namedChildren.filter((child) => child.type != "comment")
436
+
437
+ if (expressions.length != 1) return unsupportedSyntax("python", "return without one expression", location)
438
+
439
+ return {expression: convertExpression(expressions[0], filename, source), kind: "ReturnStatement", location}
440
+ }
441
+ if (node.type == "if_statement") return convertIf(node, context, visible, filename, source)
442
+ if (node.type == "expression_statement") {
443
+ const expressions = node.namedChildren.filter((child) => child.type != "comment")
444
+
445
+ if (expressions.length != 1) return unsupportedSyntax("python", node.type, location)
446
+ if (expressions[0].type == "assignment") return convertAssignment(expressions[0], context, visible, filename, source)
447
+
448
+ return convertPrint(expressions[0], filename, source, location)
449
+ }
450
+
451
+ return unsupportedSyntax("python", node.type, location)
452
+ }
453
+
454
+ /**
455
+ * Converts the exact print(expression) statement.
456
+ * @param {import("tree-sitter").SyntaxNode} node - Expression statement value.
457
+ * @param {string} filename - Source filename.
458
+ * @param {string} source - Complete source.
459
+ * @param {import("../semantic/types.js").SourceLocation} location - Whole statement location.
460
+ * @returns {import("../semantic/types.js").PrintStatement} Print statement.
461
+ */
462
+ function convertPrint(node, filename, source, location) {
463
+ if (node.type != "call") return unsupportedSyntax("python", "entry expression other than print", nodeLocation(node, filename, source))
464
+ const callee = requiredField(node, "function", filename, source)
465
+ const argumentsNode = requiredField(node, "arguments", filename, source)
466
+ const arguments_ = argumentsNode.namedChildren.filter((child) => child.type != "comment")
467
+
468
+ if (callee.type != "identifier" || callee.text != "print" || argumentsNode.type != "argument_list" || arguments_.length != 1) {
469
+ return unsupportedSyntax("python", "print call shape", nodeLocation(node, filename, source))
470
+ }
471
+
472
+ return {expression: convertExpression(arguments_[0], filename, source), kind: "PrintStatement", location}
473
+ }
474
+
475
+ /**
476
+ * Converts one if/elif/else node into nested semantic blocks.
477
+ * @param {import("tree-sitter").SyntaxNode} node - If or elif node.
478
+ * @param {PythonContext} context - Comment state.
479
+ * @param {Set<string>} visible - Visible bindings in this lexical scope.
480
+ * @param {string} filename - Source filename.
481
+ * @param {string} source - Complete source.
482
+ * @returns {import("../semantic/types.js").IfStatement} Semantic branch.
483
+ */
484
+ function convertIf(node, context, visible, filename, source) {
485
+ const location = nodeLocation(node, filename, source)
486
+ const conditionNode = requiredField(node, "condition", filename, source)
487
+ const consequenceNode = requiredField(node, "consequence", filename, source)
488
+ const alternativeNodes = node.namedChildren.filter((_child, index) => node.fieldNameForNamedChild(index) == "alternative")
489
+ const alternate = convertAlternatives(alternativeNodes, context, new Set(visible), filename, source)
490
+
491
+ return {
492
+ ...(alternate ? {alternate} : {}),
493
+ condition: convertExpression(conditionNode, filename, source),
494
+ consequent: convertBlock(consequenceNode, context, new Set(visible), filename, source),
495
+ kind: "IfStatement",
496
+ location
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Converts an else block or nests one elif inside its semantic alternate block.
502
+ * @param {import("tree-sitter").SyntaxNode[]} nodes - Ordered elif/else nodes.
503
+ * @param {PythonContext} context - Comment state.
504
+ * @param {Set<string>} visible - Visible bindings in this lexical scope.
505
+ * @param {string} filename - Source filename.
506
+ * @param {string} source - Complete source.
507
+ * @returns {import("../semantic/types.js").Block | undefined} Alternate block.
508
+ */
509
+ function convertAlternatives(nodes, context, visible, filename, source) {
510
+ /** @type {import("../semantic/types.js").Block | undefined} */
511
+ let alternate
512
+
513
+ for (let index = nodes.length - 1; index >= 0; index -= 1) {
514
+ const node = nodes[index]
515
+
516
+ if (node.type == "else_clause") {
517
+ if (alternate) return unsupportedSyntax("python", "multiple else clauses", nodeLocation(node, filename, source))
518
+ alternate = convertBlock(requiredField(node, "body", filename, source), context, new Set(visible), filename, source)
519
+ continue
520
+ }
521
+ if (node.type != "elif_clause") return unsupportedSyntax("python", node.type, nodeLocation(node, filename, source))
522
+ const nested = convertIf(node, context, new Set(visible), filename, source)
523
+
524
+ if (alternate) nested.alternate = alternate
525
+ alternate = {kind: "Block", location: nodeLocation(node, filename, source), statements: [nested]}
526
+ }
527
+
528
+ return alternate
529
+ }
530
+
531
+ /**
532
+ * Converts one indentation block and permits pass only as its sole executable node.
533
+ * @param {import("tree-sitter").SyntaxNode} node - Block node.
534
+ * @param {PythonContext} context - Comment state.
535
+ * @param {Set<string>} visible - Visible bindings in this lexical scope.
536
+ * @param {string} filename - Source filename.
537
+ * @param {string} source - Complete source.
538
+ * @returns {import("../semantic/types.js").Block} Semantic block.
539
+ */
540
+ function convertBlock(node, context, visible, filename, source) {
541
+ const location = nodeLocation(node, filename, source)
542
+
543
+ if (node.type != "block") return unsupportedSyntax("python", node.type, location)
544
+ const statements = node.namedChildren.filter((child) => child.type != "comment")
545
+
546
+ if (statements.some((statement) => statement.type == "pass_statement")) {
547
+ if (statements.length != 1) return unsupportedSyntax("python", "pass with executable siblings", location)
548
+
549
+ return {kind: "Block", location, statements: []}
550
+ }
551
+
552
+ return {kind: "Block", location, statements: statements.map((statement) => convertStatement(statement, context, visible, filename, source))}
553
+ }
554
+
555
+ /**
556
+ * Converts one exact synchronous top-level function.
557
+ * @param {import("tree-sitter").SyntaxNode} node - Function node.
558
+ * @param {PythonContext} context - Comment state.
559
+ * @param {string} filename - Source filename.
560
+ * @param {string} source - Complete source.
561
+ * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
562
+ */
563
+ function convertFunction(node, context, filename, source) {
564
+ const location = nodeLocation(node, filename, source)
565
+
566
+ if (node.type != "function_definition" || node.children.some((child) => !child.isNamed && child.type == "async")) {
567
+ return unsupportedSyntax("python", node.type, location)
568
+ }
569
+ const typeParametersNode = node.childForFieldName("type_parameters")
570
+
571
+ if (typeParametersNode) {
572
+ return unsupportedSyntax("python", typeParametersNode.type, nodeLocation(typeParametersNode, filename, source))
573
+ }
574
+ const nameNode = requiredField(node, "name", filename, source)
575
+ const parametersNode = requiredField(node, "parameters", filename, source)
576
+ const returnNode = node.childForFieldName("return_type")
577
+ const bodyNode = requiredField(node, "body", filename, source)
578
+ const name = identifier(nameNode, "function", filename, source)
579
+ const parameterNodes = parametersNode.namedChildren.filter((child) => child.type != "comment")
580
+
581
+ if (parametersNode.children.some((child) => !child.isNamed && !["(", ")", ","].includes(child.type))) {
582
+ return unsupportedSyntax("python", "parameter separators", nodeLocation(parametersNode, filename, source))
583
+ }
584
+ const parameters = parameterNodes.map((parameterNode) => {
585
+ const parameterLocation = nodeLocation(parameterNode, filename, source)
586
+
587
+ if (parameterNode.type != "typed_parameter") {
588
+ if (parameterNode.type == "identifier") return missingType("python", `Parameter '${parameterNode.text}'`, parameterLocation)
589
+
590
+ return unsupportedSyntax("python", parameterNode.type, parameterLocation)
591
+ }
592
+ const parameterNameNode = parameterNode.namedChildren.find((child) => child.type == "identifier")
593
+ const typeNode = parameterNode.childForFieldName("type")
594
+
595
+ if (!parameterNameNode) return unsupportedSyntax("python", "parameter without identifier", parameterLocation)
596
+ const parameterName = identifier(parameterNameNode, "parameter", filename, source)
597
+
598
+ return withParserRanges({
599
+ kind: /** @type {const} */ ("Parameter"),
600
+ location: parameterLocation,
601
+ name: parameterName,
602
+ type: convertType(typeNode, `Parameter '${parameterName}'`, parameterLocation, filename, source)
603
+ }, {name: nodeLocation(parameterNameNode, filename, source)})
604
+ })
605
+
606
+ const visible = new Set(parameters.map((parameter) => parameter.name))
607
+
608
+ return withParserRanges({
609
+ body: convertBlock(bodyNode, context, visible, filename, source),
610
+ kind: "FunctionDeclaration",
611
+ location,
612
+ name,
613
+ parameters,
614
+ returnType: convertType(returnNode, `Function '${name}' return`, location, filename, source)
615
+ }, {name: nodeLocation(nameNode, filename, source)})
616
+ }
617
+
618
+ /**
619
+ * Parses the strict Task 016 Python profile.
620
+ * @param {{filename: string, source: string}} input - Parser input.
621
+ * @returns {import("../semantic/types.js").SemanticModule} Semantic module.
622
+ */
623
+ export function parsePython({filename, source}) {
624
+ if (!hasOnlyUnicodeScalars(source)) {
625
+ const invalidOffset = firstLoneSurrogateOffset(source)
626
+
627
+ throw new SemantifoldDiagnostic({
628
+ code: "PARSE_ERROR",
629
+ language: "python",
630
+ location: locationFromOffsets(filename, source, invalidOffset, invalidOffset + 1),
631
+ message: "Python source contains an invalid lone UTF-16 surrogate."
632
+ })
633
+ }
634
+ let tree
635
+
636
+ try {
637
+ tree = parser.parse(source)
638
+ } catch (error) {
639
+ return parseFailure("python", error)
640
+ }
641
+ const root = tree.rootNode
642
+
643
+ validateParserTree(root, filename, source)
644
+ if (root.type != "module") throw new Error(`Tree-sitter returned Python root '${root.type}'.`)
645
+
646
+ const comments = root.descendantsOfType("comment")
647
+ const context = {comments, usedImmutableMarkers: new Set()}
648
+ const functions = []
649
+ const entryNodes = []
650
+ let sawEntry = false
651
+
652
+ for (const node of root.namedChildren) {
653
+ if (node.type == "comment") continue
654
+ if (node.type == "function_definition" && !sawEntry) functions.push(convertFunction(node, context, filename, source))
655
+ else {
656
+ sawEntry = true
657
+ entryNodes.push(node)
658
+ }
659
+ }
660
+ const location = moduleLocation(filename, source)
661
+ const entryLocation = entryNodes.length == 0 ? location : locationFromOffsets(
662
+ filename,
663
+ source,
664
+ bindingIndexToUtf16Offset(source, entryNodes[0].startIndex),
665
+ bindingIndexToUtf16Offset(source, entryNodes.at(-1)?.endIndex ?? root.endIndex)
666
+ )
667
+ const entryVisible = new Set()
668
+ const entryHasPass = entryNodes.some((node) => node.type == "pass_statement")
669
+
670
+ if (entryHasPass && entryNodes.length != 1) {
671
+ return unsupportedSyntax("python", "top-level pass with executable siblings", entryLocation)
672
+ }
673
+ const entryBlock = {
674
+ kind: /** @type {const} */ ("Block"),
675
+ location: entryLocation,
676
+ statements: entryHasPass ? [] : entryNodes.map((node) => convertStatement(node, context, entryVisible, filename, source))
677
+ }
678
+ const unusedMarker = comments.find((comment) => comment.text == immutableMarker && !context.usedImmutableMarkers.has(comment))
679
+
680
+ if (unusedMarker) return unsupportedSyntax("python", "unattached immutable marker", nodeLocation(unusedMarker, filename, source))
681
+ if (functions.length == 0) return unsupportedSyntax("python", "module without a function", location)
682
+
683
+ return {
684
+ entryPoint: {body: entryBlock, kind: "EntryPoint", location: entryLocation},
685
+ functions,
686
+ kind: "Module",
687
+ location
688
+ }
689
+ }