semantifold 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +71 -0
  3. package/build/index.d.ts +54 -0
  4. package/build/index.d.ts.map +1 -0
  5. package/build/index.js +45 -0
  6. package/build/src/backends/identifiers.d.ts +19 -0
  7. package/build/src/backends/identifiers.d.ts.map +1 -0
  8. package/build/src/backends/identifiers.js +93 -0
  9. package/build/src/backends/index.d.ts +35 -0
  10. package/build/src/backends/index.d.ts.map +1 -0
  11. package/build/src/backends/index.js +149 -0
  12. package/build/src/backends/java.d.ts +8 -0
  13. package/build/src/backends/java.d.ts.map +1 -0
  14. package/build/src/backends/java.js +172 -0
  15. package/build/src/backends/javascript.d.ts +8 -0
  16. package/build/src/backends/javascript.d.ts.map +1 -0
  17. package/build/src/backends/javascript.js +173 -0
  18. package/build/src/backends/php.d.ts +8 -0
  19. package/build/src/backends/php.d.ts.map +1 -0
  20. package/build/src/backends/php.js +175 -0
  21. package/build/src/backends/ruby.d.ts +8 -0
  22. package/build/src/backends/ruby.d.ts.map +1 -0
  23. package/build/src/backends/ruby.js +155 -0
  24. package/build/src/backends/scalars.d.ts +15 -0
  25. package/build/src/backends/scalars.d.ts.map +1 -0
  26. package/build/src/backends/scalars.js +138 -0
  27. package/build/src/backends/shared.d.ts +26 -0
  28. package/build/src/backends/shared.d.ts.map +1 -0
  29. package/build/src/backends/shared.js +424 -0
  30. package/build/src/backends/typescript.d.ts +8 -0
  31. package/build/src/backends/typescript.d.ts.map +1 -0
  32. package/build/src/backends/typescript.js +160 -0
  33. package/build/src/backends/writer.d.ts +87 -0
  34. package/build/src/backends/writer.d.ts.map +1 -0
  35. package/build/src/backends/writer.js +207 -0
  36. package/build/src/diagnostic.d.ts +67 -0
  37. package/build/src/diagnostic.d.ts.map +1 -0
  38. package/build/src/diagnostic.js +96 -0
  39. package/build/src/frontends/index.d.ts +14 -0
  40. package/build/src/frontends/index.d.ts.map +1 -0
  41. package/build/src/frontends/index.js +36 -0
  42. package/build/src/frontends/java.d.ts +12 -0
  43. package/build/src/frontends/java.d.ts.map +1 -0
  44. package/build/src/frontends/java.js +630 -0
  45. package/build/src/frontends/javascript-typescript.d.ts +19 -0
  46. package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
  47. package/build/src/frontends/javascript-typescript.js +633 -0
  48. package/build/src/frontends/php.d.ts +12 -0
  49. package/build/src/frontends/php.d.ts.map +1 -0
  50. package/build/src/frontends/php.js +523 -0
  51. package/build/src/frontends/ruby.d.ts +12 -0
  52. package/build/src/frontends/ruby.d.ts.map +1 -0
  53. package/build/src/frontends/ruby.js +515 -0
  54. package/build/src/frontends/scalars.d.ts +19 -0
  55. package/build/src/frontends/scalars.d.ts.map +1 -0
  56. package/build/src/frontends/scalars.js +71 -0
  57. package/build/src/mapping.d.ts +160 -0
  58. package/build/src/mapping.d.ts.map +1 -0
  59. package/build/src/mapping.js +1120 -0
  60. package/build/src/semantic/location.d.ts +61 -0
  61. package/build/src/semantic/location.d.ts.map +1 -0
  62. package/build/src/semantic/location.js +142 -0
  63. package/build/src/semantic/operators.d.ts +16 -0
  64. package/build/src/semantic/operators.d.ts.map +1 -0
  65. package/build/src/semantic/operators.js +24 -0
  66. package/build/src/semantic/provenance.d.ts +74 -0
  67. package/build/src/semantic/provenance.d.ts.map +1 -0
  68. package/build/src/semantic/provenance.js +577 -0
  69. package/build/src/semantic/scalars.d.ts +19 -0
  70. package/build/src/semantic/scalars.d.ts.map +1 -0
  71. package/build/src/semantic/scalars.js +40 -0
  72. package/build/src/semantic/types.d.ts +1003 -0
  73. package/build/src/semantic/types.d.ts.map +1 -0
  74. package/build/src/semantic/types.js +279 -0
  75. package/build/src/semantic/validate.d.ts +60 -0
  76. package/build/src/semantic/validate.d.ts.map +1 -0
  77. package/build/src/semantic/validate.js +472 -0
  78. package/docs/architecture.md +39 -0
  79. package/docs/coding-standards.md +7 -0
  80. package/docs/goals.md +7 -0
  81. package/docs/language-support.md +69 -0
  82. package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
  83. package/docs/source-maps.md +134 -0
  84. package/docs/testing.md +25 -0
  85. package/index.js +63 -0
  86. package/package.json +69 -0
  87. package/src/backends/identifiers.js +101 -0
  88. package/src/backends/index.js +159 -0
  89. package/src/backends/java.js +182 -0
  90. package/src/backends/javascript.js +184 -0
  91. package/src/backends/php.js +184 -0
  92. package/src/backends/ruby.js +163 -0
  93. package/src/backends/scalars.js +120 -0
  94. package/src/backends/shared.js +456 -0
  95. package/src/backends/typescript.js +168 -0
  96. package/src/backends/writer.js +224 -0
  97. package/src/diagnostic.js +103 -0
  98. package/src/frontends/index.js +36 -0
  99. package/src/frontends/java.js +735 -0
  100. package/src/frontends/javascript-typescript.js +718 -0
  101. package/src/frontends/php.js +593 -0
  102. package/src/frontends/ruby.js +622 -0
  103. package/src/frontends/scalars.js +77 -0
  104. package/src/mapping.js +1260 -0
  105. package/src/semantic/location.js +154 -0
  106. package/src/semantic/operators.js +28 -0
  107. package/src/semantic/provenance.js +629 -0
  108. package/src/semantic/scalars.js +44 -0
  109. package/src/semantic/types.js +319 -0
  110. package/src/semantic/validate.js +529 -0
  111. package/tsconfig.json +16 -0
@@ -0,0 +1,718 @@
1
+ // @ts-check
2
+
3
+ import {parse as parseBabel} from "@babel/parser"
4
+ import {parse as parseComment} from "comment-parser"
5
+ import {missingType, parseFailure, unsupportedSyntax} from "../diagnostic.js"
6
+ import {locationFromOffsets, moduleLocation} 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
+ /** @typedef {NonNullable<import("@babel/parser").ParseResult<import("@babel/types").File>["tokens"]>[number]} BabelToken */
13
+ /** @typedef {{byStart: Map<number, BabelToken>, tokens: BabelToken[]}} BabelTokenIndex */
14
+
15
+ /** @type {WeakMap<object, BabelTokenIndex>} */
16
+ const nodeTokens = new WeakMap()
17
+ const babelBinaryOperations = new Map([
18
+ ["+", "Add"],
19
+ ["-", "Subtract"],
20
+ ["*", "Multiply"],
21
+ ["===", "Equal"],
22
+ ["!==", "NotEqual"],
23
+ ["<", "LessThan"],
24
+ ["<=", "LessThanOrEqual"],
25
+ [">", "GreaterThan"],
26
+ [">=", "GreaterThanOrEqual"]
27
+ ])
28
+
29
+ /**
30
+ * Returns a source location for a Babel node.
31
+ * @param {import("@babel/types").Node} node - Babel node.
32
+ * @param {string} filename - Source filename.
33
+ * @param {string} source - Complete source.
34
+ * @returns {import("../semantic/types.js").SourceLocation} Normalized location.
35
+ */
36
+ function nodeLocation(node, filename, source) {
37
+ if (typeof node.start != "number" || typeof node.end != "number") {
38
+ throw new Error(`Babel omitted source offsets for ${node.type}.`)
39
+ }
40
+
41
+ return locationFromOffsets(filename, source, node.start, node.end)
42
+ }
43
+
44
+ /**
45
+ * Returns the identifier spelling rather than a trailing TypeScript annotation.
46
+ * @param {import("@babel/types").Identifier} node - Parser identifier.
47
+ * @param {string} filename - Source filename.
48
+ * @param {string} source - Complete source.
49
+ * @returns {import("../semantic/types.js").SourceLocation} Identifier location.
50
+ */
51
+ function identifierLocation(node, filename, source) {
52
+ if (typeof node.start != "number") throw new Error("Babel omitted an identifier start offset.")
53
+ const token = nodeTokens.get(node)?.byStart.get(node.start)
54
+
55
+ if (!token || token.type.label != "name") throw new Error(`Babel omitted the identifier token for '${node.name}'.`)
56
+
57
+ return locationFromOffsets(filename, source, token.start, token.end)
58
+ }
59
+
60
+ /**
61
+ * Locates one Babel token within parser-node boundaries.
62
+ * @param {import("@babel/types").Node} owner - Token-owning parser node.
63
+ * @param {string} value - Exact token spelling.
64
+ * @param {number} startOffset - Inclusive boundary.
65
+ * @param {number} endOffset - Exclusive boundary.
66
+ * @param {string} filename - Source filename.
67
+ * @param {string} source - Complete source.
68
+ * @returns {import("../semantic/types.js").SourceLocation} Exact token location.
69
+ */
70
+ function tokenLocation(owner, value, startOffset, endOffset, filename, source) {
71
+ const tokens = nodeTokens.get(owner)?.tokens ?? []
72
+ let low = 0
73
+ let high = tokens.length
74
+
75
+ while (low < high) {
76
+ const middle = Math.floor((low + high) / 2)
77
+
78
+ if (tokens[middle].start < startOffset) low = middle + 1
79
+ else high = middle
80
+ }
81
+ let token
82
+
83
+ for (let index = low; index < tokens.length && tokens[index].start < endOffset; index++) {
84
+ const candidate = tokens[index]
85
+
86
+ if (candidate.end <= endOffset && (candidate.value == value || candidate.type.label == value)) {
87
+ token = candidate
88
+ break
89
+ }
90
+ }
91
+
92
+ if (!token) throw new Error(`Babel omitted token '${value}' between ${startOffset} and ${endOffset}.`)
93
+
94
+ return locationFromOffsets(filename, source, token.start, token.end)
95
+ }
96
+
97
+ /**
98
+ * Locates a comment-parser token inside a Babel-owned doc comment.
99
+ * @param {import("@babel/types").CommentBlock} comment - Babel doc comment.
100
+ * @param {import("comment-parser").Spec} tag - Parsed doc tag.
101
+ * @param {"name" | "type"} field - Token field.
102
+ * @param {string} filename - Source filename.
103
+ * @param {string} source - Complete source.
104
+ * @returns {import("../semantic/types.js").SourceLocation} Exact token location.
105
+ */
106
+ function commentTagLocation(comment, tag, field, filename, source) {
107
+ const line = tag.source[0]
108
+
109
+ if (typeof comment.start != "number" || !line) throw new Error(`Comment parser omitted the @${tag.tag} source token.`)
110
+
111
+ let lineStart = comment.start
112
+
113
+ for (let lineNumber = 0; lineNumber < line.number; lineNumber++) {
114
+ while (lineStart < source.length && source[lineStart] != "\r" && source[lineStart] != "\n") lineStart++
115
+ if (source[lineStart] == "\r" && source[lineStart + 1] == "\n") lineStart += 2
116
+ else lineStart++
117
+ }
118
+
119
+ const orderedFields = ["start", "delimiter", "postDelimiter", "tag", "postTag", "type", "postType", "name"]
120
+ const fieldIndex = orderedFields.indexOf(field)
121
+ const start = lineStart + orderedFields.slice(0, fieldIndex).reduce((length, key) =>
122
+ length + line.tokens[/** @type {keyof typeof line.tokens} */ (key)].length, 0)
123
+ const token = line.tokens[field]
124
+
125
+ return locationFromOffsets(filename, source, start, start + token.length)
126
+ }
127
+
128
+ /**
129
+ * Converts a supported Babel expression.
130
+ * @param {import("@babel/types").Expression} node - Babel expression.
131
+ * @param {"javascript" | "typescript"} language - Frontend language.
132
+ * @param {string} filename - Source filename.
133
+ * @param {string} source - Complete source.
134
+ * @returns {import("../semantic/types.js").Expression} Semantic expression.
135
+ */
136
+ function convertExpression(node, language, filename, source) {
137
+ const location = nodeLocation(node, filename, source)
138
+
139
+ if (node.type == "Identifier") {
140
+ return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: node.name}, {
141
+ name: identifierLocation(node, filename, source)
142
+ })
143
+ }
144
+
145
+ if (node.type == "NumericLiteral") {
146
+ if (!Number.isSafeInteger(node.value)) return unsupportedSyntax(language, "non-safe integer literal", location)
147
+
148
+ return withParserRanges({kind: /** @type {const} */ ("IntegerLiteral"), location, value: node.value}, {literal: location})
149
+ }
150
+
151
+ if (node.type == "BooleanLiteral") {
152
+ if (typeof node.value != "boolean") return unsupportedSyntax(language, "invalid boolean literal value", location)
153
+
154
+ return withParserRanges({kind: /** @type {const} */ ("BooleanLiteral"), location, value: node.value}, {literal: location})
155
+ }
156
+
157
+ if (node.type == "StringLiteral") {
158
+ if (typeof node.value != "string" || !hasOnlyUnicodeScalars(node.value)) {
159
+ return unsupportedSyntax(language, "invalid Unicode string literal", location)
160
+ }
161
+
162
+ return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: node.value}, {literal: location})
163
+ }
164
+
165
+ if (node.type == "TemplateLiteral") {
166
+ if (node.expressions.length > 0) return unsupportedSyntax(language, "interpolated string", location)
167
+
168
+ const quasi = node.quasis[0]
169
+
170
+ if (typeof quasi.value.cooked != "string" || !hasOnlyUnicodeScalars(quasi.value.cooked)) {
171
+ return unsupportedSyntax(language, "invalid Unicode string literal", location)
172
+ }
173
+
174
+ return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: quasi.value.cooked}, {literal: location})
175
+ }
176
+
177
+ if (node.type == "UnaryExpression" && ["!", "-"].includes(node.operator)) {
178
+ const operand = convertExpression(node.argument, language, filename, source)
179
+ const semantic = withAdaptedOperation(withParserRanges({
180
+ kind: "UnaryExpression",
181
+ location,
182
+ operand
183
+ }, {
184
+ operator: tokenLocation(node, node.operator, node.start ?? 0, node.argument.start ?? node.end ?? source.length, filename, source)
185
+ }), node.operator == "!" ? "Not" : "Negate")
186
+
187
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
188
+ }
189
+
190
+ if (node.type == "LogicalExpression" && ["&&", "||"].includes(node.operator)) {
191
+ const semantic = withAdaptedOperation(withParserRanges({
192
+ kind: "BinaryExpression",
193
+ left: convertExpression(node.left, language, filename, source),
194
+ location,
195
+ right: convertExpression(node.right, language, filename, source)
196
+ }, {
197
+ operator: tokenLocation(node, node.operator, node.left.end ?? node.start ?? 0, node.right.start ?? node.end ?? source.length, filename, source)
198
+ }), node.operator == "&&" ? "And" : "Or")
199
+
200
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
201
+ }
202
+
203
+ if (node.type == "BinaryExpression" && babelBinaryOperations.has(node.operator)) {
204
+ if (node.left.type == "PrivateName") unsupportedSyntax(language, node.left.type, location)
205
+
206
+ const left = convertExpression(node.left, language, filename, source)
207
+ const right = convertExpression(node.right, language, filename, source)
208
+
209
+ const semantic = withAdaptedOperation(withParserRanges({
210
+ kind: "BinaryExpression",
211
+ left,
212
+ location,
213
+ right
214
+ }, {
215
+ operator: tokenLocation(node, node.operator, node.left.end ?? node.start ?? 0, node.right.start ?? node.end ?? source.length, filename, source)
216
+ }), /** @type {import("../semantic/operators.js").AdaptedOperation} */ (babelBinaryOperations.get(node.operator)))
217
+
218
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
219
+ }
220
+
221
+ if (node.type == "BinaryExpression" && ["==", "!="].includes(node.operator)) {
222
+ return unsupportedSyntax(language, `coercive equality ${node.operator}`, location)
223
+ }
224
+
225
+ if (node.type == "CallExpression" && node.callee.type == "Identifier") {
226
+ const arguments_ = node.arguments.map((argument) => {
227
+ if (argument.type == "SpreadElement" || argument.type == "ArgumentPlaceholder") {
228
+ return unsupportedSyntax(language, argument.type, nodeLocation(argument, filename, source))
229
+ }
230
+
231
+ return convertExpression(argument, language, filename, source)
232
+ })
233
+
234
+ return withParserRanges({arguments: arguments_, callee: node.callee.name, kind: /** @type {const} */ ("CallExpression"), location}, {
235
+ callee: identifierLocation(node.callee, filename, source)
236
+ })
237
+ }
238
+
239
+ return unsupportedSyntax(language, node.type, location)
240
+ }
241
+
242
+ /**
243
+ * Converts a supported return statement.
244
+ * @param {import("@babel/types").Statement} node - Babel statement.
245
+ * @param {"javascript" | "typescript"} language - Frontend language.
246
+ * @param {string} filename - Source filename.
247
+ * @param {string} source - Complete source.
248
+ * @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
249
+ */
250
+ function convertReturn(node, language, filename, source) {
251
+ const location = nodeLocation(node, filename, source)
252
+
253
+ if (node.type != "ReturnStatement" || !node.argument) return unsupportedSyntax(language, node.type, location)
254
+
255
+ return {expression: convertExpression(node.argument, language, filename, source), kind: "ReturnStatement", location}
256
+ }
257
+
258
+ /**
259
+ * Converts one supported local declaration or assignment.
260
+ * @param {import("@babel/types").Statement} node - Babel statement.
261
+ * @param {"javascript" | "typescript"} language - Frontend language.
262
+ * @param {string} filename - Source filename.
263
+ * @param {string} source - Complete source.
264
+ * @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
265
+ */
266
+ function convertLocalStatement(node, language, filename, source) {
267
+ const location = nodeLocation(node, filename, source)
268
+
269
+ if (node.type == "VariableDeclaration") {
270
+ if (!["let", "const"].includes(node.kind) || node.declarations.length != 1) {
271
+ return unsupportedSyntax(language, `${node.kind} declaration`, location)
272
+ }
273
+
274
+ const declarator = node.declarations[0]
275
+ const declaratorLocation = nodeLocation(declarator, filename, source)
276
+
277
+ if (declarator.id.type != "Identifier") return unsupportedSyntax(language, declarator.id.type, declaratorLocation)
278
+ if (!declarator.init) return unsupportedSyntax(language, "uninitialized declaration", declaratorLocation)
279
+
280
+ const type = language == "typescript"
281
+ ? convertTypeScriptType(declarator.id.typeAnnotation, `Local '${declarator.id.name}'`, declaratorLocation, filename, source)
282
+ : localJavaScriptType(node, declarator.id.name, filename, source)
283
+
284
+ return withParserRanges({
285
+ initializer: convertExpression(declarator.init, language, filename, source),
286
+ kind: "LocalDeclaration",
287
+ location,
288
+ mutable: node.kind == "let",
289
+ name: declarator.id.name,
290
+ type
291
+ }, {
292
+ name: identifierLocation(declarator.id, filename, source),
293
+ operator: tokenLocation(declarator, "=", declarator.id.end ?? declarator.start ?? 0, declarator.init.start ?? declarator.end ?? source.length, filename, source)
294
+ })
295
+ }
296
+
297
+ if (node.type == "ExpressionStatement" && node.expression.type == "AssignmentExpression") {
298
+ const assignment = node.expression
299
+ const targetLocation = nodeLocation(assignment.left, filename, source)
300
+
301
+ if (assignment.operator != "=") return unsupportedSyntax(language, `assignment ${assignment.operator}`, location)
302
+ if (assignment.left.type != "Identifier") return unsupportedSyntax(language, assignment.left.type, targetLocation)
303
+
304
+ const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: assignment.left.name}, {
305
+ name: identifierLocation(assignment.left, filename, source)
306
+ })
307
+
308
+ return withParserRanges({
309
+ expression: convertExpression(assignment.right, language, filename, source),
310
+ kind: "AssignmentStatement",
311
+ location,
312
+ target
313
+ }, {
314
+ operator: tokenLocation(assignment, "=", assignment.left.end ?? assignment.start ?? 0, assignment.right.start ?? assignment.end ?? source.length, filename, source)
315
+ })
316
+ }
317
+
318
+ return unsupportedSyntax(language, node.type, location)
319
+ }
320
+
321
+ /**
322
+ * Reads an immediately associated JavaScript local `@type` tag.
323
+ * @param {import("@babel/types").VariableDeclaration} node - Local declaration.
324
+ * @param {string} name - Local name.
325
+ * @param {string} filename - Source filename.
326
+ * @param {string} source - Complete source.
327
+ * @returns {import("../semantic/types.js").TypeReference} Semantic type.
328
+ */
329
+ function localJavaScriptType(node, name, filename, source) {
330
+ const comment = node.leadingComments?.at(-1)
331
+ const location = nodeLocation(node, filename, source)
332
+
333
+ if (!comment || comment.type != "CommentBlock" || !comment.value.startsWith("*")) {
334
+ return missingType("javascript", `Local '${name}'`, location)
335
+ }
336
+
337
+ const gap = source.slice(comment.end ?? 0, node.start ?? 0)
338
+
339
+ if (!/^\s*$/u.test(gap) || (gap.match(/\n/gu)?.length ?? 0) > 1) {
340
+ return missingType("javascript", `Local '${name}'`, location)
341
+ }
342
+
343
+ const block = parseComment(`/*${comment.value}*/`)[0]
344
+ const tags = block?.tags.filter((tag) => tag.tag == "type") ?? []
345
+
346
+ const tag = tags.length == 1 ? tags[0] : undefined
347
+
348
+ return convertType(
349
+ tag?.type,
350
+ "javascript",
351
+ `Local '${name}'`,
352
+ location,
353
+ tag ? commentTagLocation(comment, tag, "type", filename, source) : location
354
+ )
355
+ }
356
+
357
+ /**
358
+ * Converts one exhaustive Babel statement.
359
+ * @param {import("@babel/types").Statement} node - Babel statement.
360
+ * @param {"javascript" | "typescript"} language - Frontend language.
361
+ * @param {string} filename - Source filename.
362
+ * @param {string} source - Complete source.
363
+ * @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
364
+ * @returns {import("../semantic/types.js").Statement} Semantic statement.
365
+ */
366
+ function convertStatement(node, language, filename, source, canonicalZeroRequired) {
367
+ if (node.type == "ReturnStatement") return convertReturn(node, language, filename, source)
368
+ if (node.type == "IfStatement") return convertIf(node, language, filename, source, canonicalZeroRequired)
369
+ if (node.type == "VariableDeclaration" || node.type == "ExpressionStatement" && node.expression.type == "AssignmentExpression") {
370
+ return convertLocalStatement(node, language, filename, source)
371
+ }
372
+ if (node.type == "ExpressionStatement") return convertPrint(node, language, filename, source, canonicalZeroRequired)
373
+
374
+ return unsupportedSyntax(language, node.type, nodeLocation(node, filename, source))
375
+ }
376
+
377
+ /**
378
+ * Converts one Babel block or normalized single-statement body.
379
+ * @param {import("@babel/types").BlockStatement | import("@babel/types").Statement[]} input - Parser block or statements.
380
+ * @param {"javascript" | "typescript"} language - Frontend language.
381
+ * @param {string} filename - Source filename.
382
+ * @param {string} source - Complete source.
383
+ * @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
384
+ * @param {import("../semantic/types.js").SourceLocation} [location] - Location for a statement-array block.
385
+ * @returns {import("../semantic/types.js").Block} Semantic block.
386
+ */
387
+ function convertBlock(input, language, filename, source, canonicalZeroRequired, location) {
388
+ const statements = Array.isArray(input) ? input : input.body
389
+ const directives = Array.isArray(input) ? [] : input.directives
390
+ const blockLocation = location ?? nodeLocation(/** @type {import("@babel/types").BlockStatement} */ (input), filename, source)
391
+
392
+ if (directives.length > 0) {
393
+ return unsupportedSyntax(language, "directive", nodeLocation(directives[0], filename, source))
394
+ }
395
+
396
+ return {
397
+ kind: "Block",
398
+ location: blockLocation,
399
+ statements: statements.map((statement) => convertStatement(statement, language, filename, source, canonicalZeroRequired))
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Reads JavaScript JSDoc types from a function.
405
+ * @param {import("@babel/types").FunctionDeclaration} node - Babel function node.
406
+ * @param {string} filename - Source filename.
407
+ * @param {string} source - Complete source.
408
+ * @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined}} Parsed types.
409
+ */
410
+ function jsdocTypes(node, filename, source) {
411
+ const comment = node.leadingComments?.find((candidate) => candidate.type == "CommentBlock" && candidate.value.startsWith("*"))
412
+
413
+ if (!comment) missingType("javascript", `Function '${node.id?.name ?? "anonymous"}'`, nodeLocation(node, filename, source))
414
+
415
+ const commentBlock = /** @type {import("@babel/types").CommentBlock} */ (comment)
416
+ const block = parseComment(`/*${commentBlock.value}*/`)[0]
417
+ const parameters = new Map(block.tags.filter((tag) => tag.tag == "param").map((tag) => [tag.name, {
418
+ location: commentTagLocation(commentBlock, tag, "type", filename, source),
419
+ sourceType: tag.type
420
+ }]))
421
+ const returnTag = block.tags.find((tag) => tag.tag == "returns" || tag.tag == "return")
422
+
423
+ return {
424
+ parameters,
425
+ returnType: returnTag ? {location: commentTagLocation(commentBlock, returnTag, "type", filename, source), sourceType: returnTag.type} : undefined
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Requires a supported JavaScript JSDoc scalar type spelling.
431
+ * @param {string | undefined} sourceType - Source-language type.
432
+ * @param {"javascript" | "typescript"} language - Frontend language.
433
+ * @param {string} subject - Typed subject.
434
+ * @param {import("../semantic/types.js").SourceLocation} location - Source location.
435
+ * @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type token location.
436
+ * @returns {import("../semantic/types.js").TypeReference} Semantic type.
437
+ */
438
+ function convertType(sourceType, language, subject, location, typeLocation = location) {
439
+ return requireSourceScalarType(language, sourceType, subject, location, typeLocation)
440
+ }
441
+
442
+ /**
443
+ * Converts one exact TypeScript scalar keyword annotation.
444
+ * @param {import("@babel/types").TypeAnnotation | import("@babel/types").TSTypeAnnotation | import("@babel/types").Noop | null | undefined} annotation - Type annotation.
445
+ * @param {string} subject - Typed subject.
446
+ * @param {import("../semantic/types.js").SourceLocation} ownerLocation - Owning declaration location.
447
+ * @param {string} filename - Source filename.
448
+ * @param {string} source - Complete source.
449
+ * @returns {import("../semantic/types.js").TypeReference} Semantic scalar type.
450
+ */
451
+ function convertTypeScriptType(annotation, subject, ownerLocation, filename, source) {
452
+ if (!annotation) return missingType("typescript", subject, ownerLocation)
453
+ if (annotation.type != "TSTypeAnnotation") {
454
+ return unsupportedSyntax("typescript", "unsupported scalar type", nodeLocation(annotation, filename, source))
455
+ }
456
+
457
+ const typeNode = annotation.typeAnnotation
458
+ const sourceType = typeNode.type == "TSNumberKeyword"
459
+ ? "number"
460
+ : typeNode.type == "TSBooleanKeyword"
461
+ ? "boolean"
462
+ : typeNode.type == "TSStringKeyword" ? "string" : undefined
463
+
464
+ if (!sourceType) {
465
+ return unsupportedSyntax("typescript", "unsupported scalar type", nodeLocation(typeNode, filename, source))
466
+ }
467
+
468
+ return requireSourceScalarType("typescript", sourceType, subject, ownerLocation, nodeLocation(typeNode, filename, source))
469
+ }
470
+
471
+ /**
472
+ * Converts a supported function declaration.
473
+ * @param {import("@babel/types").FunctionDeclaration} node - Babel function.
474
+ * @param {"javascript" | "typescript"} language - Frontend language.
475
+ * @param {string} filename - Source filename.
476
+ * @param {string} source - Complete source.
477
+ * @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
478
+ * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
479
+ */
480
+ function convertFunction(node, language, filename, source, canonicalZeroRequired) {
481
+ const location = nodeLocation(node, filename, source)
482
+
483
+ if (!node.id) return unsupportedSyntax(language, "anonymous function", location)
484
+ if (node.async) return unsupportedSyntax(language, "async function", location)
485
+ if (node.generator) return unsupportedSyntax(language, "generator function", location)
486
+
487
+ const documentedTypes = language == "javascript" ? jsdocTypes(node, filename, source) : undefined
488
+ const parameters = node.params.map((parameter) => {
489
+ const parameterLocation = nodeLocation(parameter, filename, source)
490
+
491
+ if (parameter.type != "Identifier") return unsupportedSyntax(language, parameter.type, parameterLocation)
492
+
493
+ const documentedType = documentedTypes?.parameters.get(parameter.name)
494
+ const type = language == "javascript"
495
+ ? convertType(documentedType?.sourceType, language, `Parameter '${parameter.name}'`, parameterLocation, documentedType?.location)
496
+ : convertTypeScriptType(parameter.typeAnnotation, `Parameter '${parameter.name}'`, parameterLocation, filename, source)
497
+
498
+ return withParserRanges({
499
+ kind: /** @type {const} */ ("Parameter"),
500
+ location: parameterLocation,
501
+ name: parameter.name,
502
+ type
503
+ }, {name: identifierLocation(parameter, filename, source)})
504
+ })
505
+ const returnAnnotation = node.returnType
506
+ const returnType = language == "javascript"
507
+ ? convertType(documentedTypes?.returnType?.sourceType, language, `Function '${node.id.name}' return`, location, documentedTypes?.returnType?.location)
508
+ : convertTypeScriptType(returnAnnotation, `Function '${node.id.name}' return`, location, filename, source)
509
+ const body = convertBlock(node.body, language, filename, source, canonicalZeroRequired)
510
+
511
+ return withParserRanges({
512
+ body,
513
+ kind: "FunctionDeclaration",
514
+ location,
515
+ name: node.id.name,
516
+ parameters,
517
+ returnType
518
+ }, {name: identifierLocation(node.id, filename, source)})
519
+ }
520
+
521
+ /**
522
+ * Converts the existing exact if/else terminal shape.
523
+ * @param {import("@babel/types").IfStatement} node - Babel if statement.
524
+ * @param {"javascript" | "typescript"} language - Frontend language.
525
+ * @param {string} filename - Source filename.
526
+ * @param {string} source - Complete source.
527
+ * @param {boolean} canonicalZeroRequired - Whether generated scalar output may contain signed zero.
528
+ * @returns {import("../semantic/types.js").IfStatement} Semantic branch.
529
+ */
530
+ function convertIf(node, language, filename, source, canonicalZeroRequired) {
531
+ const location = nodeLocation(node, filename, source)
532
+ const consequent = node.consequent.type == "BlockStatement"
533
+ ? convertBlock(node.consequent, language, filename, source, canonicalZeroRequired)
534
+ : convertBlock([node.consequent], language, filename, source, canonicalZeroRequired, nodeLocation(node.consequent, filename, source))
535
+ const alternate = node.alternate
536
+ ? node.alternate.type == "BlockStatement"
537
+ ? convertBlock(node.alternate, language, filename, source, canonicalZeroRequired)
538
+ : convertBlock([node.alternate], language, filename, source, canonicalZeroRequired, nodeLocation(node.alternate, filename, source))
539
+ : undefined
540
+
541
+ return {
542
+ ...(alternate ? {alternate} : {}),
543
+ condition: convertExpression(node.test, language, filename, source),
544
+ consequent,
545
+ kind: "IfStatement",
546
+ location
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Reports whether Babel parsed an operation that makes generated JavaScript-family scalar output require zero canonicalization.
552
+ * @param {unknown} value - Babel parser subtree.
553
+ * @param {WeakSet<object>} [visited] - Cycle protection.
554
+ * @returns {boolean} Whether the subtree contains integer negation or multiplication syntax.
555
+ */
556
+ function parserTreeRequiresCanonicalZero(value, visited = new WeakSet()) {
557
+ if (!value || typeof value != "object") return false
558
+ if (visited.has(value)) return false
559
+
560
+ visited.add(value)
561
+ if (Reflect.get(value, "type") == "UnaryExpression" && Reflect.get(value, "operator") == "-") return true
562
+ if (Reflect.get(value, "type") == "BinaryExpression" && Reflect.get(value, "operator") == "*") return true
563
+
564
+ return Object.entries(value).some(([key, child]) =>
565
+ !["extra", "innerComments", "leadingComments", "loc", "trailingComments"].includes(key) &&
566
+ (Array.isArray(child)
567
+ ? child.some((item) => parserTreeRequiresCanonicalZero(item, visited))
568
+ : parserTreeRequiresCanonicalZero(child, visited)))
569
+ }
570
+
571
+ /**
572
+ * Recognizes the parser-owned receiver parentheses emitted only for canonical integer output.
573
+ * @param {import("@babel/types").Expression | import("@babel/types").Super} receiver - Qualified-call receiver.
574
+ * @param {import("@babel/types").CallExpression} call - Outer `toString` call.
575
+ * @returns {boolean} Whether the receiver has the generated wrapper shape.
576
+ */
577
+ function hasCanonicalZeroReceiverWrapper(receiver, call) {
578
+ const extra = Reflect.get(receiver, "extra")
579
+ const expressionParentheses = ["BinaryExpression", "LogicalExpression", "UnaryExpression"].includes(receiver.type) ? 1 : 0
580
+ const wrapperParentheses = expressionParentheses + 1
581
+ const property = call.callee.type == "MemberExpression" ? call.callee.property : undefined
582
+
583
+ return Boolean(extra) && typeof extra == "object" && Reflect.get(extra, "parenthesized") === true &&
584
+ Reflect.get(extra, "parenStart") === call.start && typeof call.start == "number" && typeof call.end == "number" &&
585
+ typeof receiver.start == "number" && typeof receiver.end == "number" && property?.type == "Identifier" &&
586
+ typeof property.start == "number" && typeof property.end == "number" && call.callee.start === call.start &&
587
+ receiver.start == call.start + wrapperParentheses && property.start == receiver.end + wrapperParentheses + 1 &&
588
+ property.end == property.start + "toString".length && call.end == property.end + 2
589
+ }
590
+
591
+ /**
592
+ * Converts a console.log entry-point call.
593
+ * @param {import("@babel/types").Statement} node - Babel statement.
594
+ * @param {"javascript" | "typescript"} language - Frontend language.
595
+ * @param {string} filename - Source filename.
596
+ * @param {string} source - Complete source.
597
+ * @param {boolean} canonicalZeroRequired - Whether this parsed module contains a sign-producing integer operation.
598
+ * @returns {import("../semantic/types.js").PrintStatement} Print statement.
599
+ */
600
+ function convertPrint(node, language, filename, source, canonicalZeroRequired) {
601
+ const location = nodeLocation(node, filename, source)
602
+
603
+ if (node.type != "ExpressionStatement" || node.expression.type != "CallExpression") {
604
+ return unsupportedSyntax(language, node.type, location)
605
+ }
606
+
607
+ const call = node.expression
608
+ const callee = call.callee
609
+
610
+ if (callee.type != "MemberExpression" || callee.computed || callee.object.type != "Identifier" ||
611
+ callee.object.name != "console" || callee.property.type != "Identifier" || callee.property.name != "log" ||
612
+ call.arguments.length != 1) {
613
+ return unsupportedSyntax(language, "entry-point expression", location)
614
+ }
615
+
616
+ let argument = call.arguments[0]
617
+
618
+ if (argument.type == "SpreadElement" || argument.type == "ArgumentPlaceholder") {
619
+ return unsupportedSyntax(language, argument.type, nodeLocation(argument, filename, source))
620
+ }
621
+
622
+ if (argument.type == "CallExpression" && argument.callee.type == "MemberExpression" && !argument.callee.computed &&
623
+ !argument.callee.optional && argument.callee.object.type != "Super" && argument.callee.property.type == "Identifier" &&
624
+ argument.callee.property.name == "toString" && !argument.optional && !argument.typeArguments &&
625
+ !argument.typeParameters && argument.arguments.length == 0) {
626
+ if (!canonicalZeroRequired || !hasCanonicalZeroReceiverWrapper(argument.callee.object, argument)) {
627
+ return unsupportedSyntax(language, "entry-point qualified call", nodeLocation(argument.callee.property, filename, source))
628
+ }
629
+ argument = argument.callee.object
630
+ }
631
+
632
+ return {expression: convertExpression(argument, language, filename, source), kind: "PrintStatement", location}
633
+ }
634
+
635
+ /**
636
+ * Parses JavaScript or TypeScript into the shared semantic module.
637
+ * @param {object} input - Parser input.
638
+ * @param {string} input.filename - Source filename.
639
+ * @param {"javascript" | "typescript"} input.language - Frontend language.
640
+ * @param {string} input.source - Source text.
641
+ * @returns {import("../semantic/types.js").SemanticModule} Semantic module.
642
+ */
643
+ export function parseJavaScriptTypeScript({filename, language, source}) {
644
+ const file = parseBabelSource({filename, language, source})
645
+ const tokens = file.tokens ?? []
646
+ const tokenIndex = {byStart: new Map(tokens.map((token) => [token.start, token])), tokens}
647
+ const canonicalZeroRequired = parserTreeRequiresCanonicalZero(file.program)
648
+
649
+ rememberTokens(file, tokenIndex)
650
+ if (file.program.directives.length > 0) {
651
+ return unsupportedSyntax(language, "top-level directive", nodeLocation(file.program.directives[0], filename, source))
652
+ }
653
+ const functions = file.program.body.filter((node) => node.type == "FunctionDeclaration")
654
+ .map((node) => convertFunction(node, language, filename, source, canonicalZeroRequired))
655
+ const entryNodes = file.program.body.filter((node) => node.type != "FunctionDeclaration")
656
+ const location = moduleLocation(filename, source)
657
+
658
+ if (functions.length == 0) return unsupportedSyntax(language, "module without a function", location)
659
+ const entryLocation = entryNodes.length == 0 ? location : locationFromOffsets(
660
+ filename,
661
+ source,
662
+ entryNodes[0].start ?? 0,
663
+ entryNodes.at(-1)?.end ?? source.length
664
+ )
665
+ const entryBlock = convertBlock(entryNodes, language, filename, source, canonicalZeroRequired, entryLocation)
666
+
667
+ return {
668
+ entryPoint: {
669
+ body: entryBlock,
670
+ kind: "EntryPoint",
671
+ location: entryLocation
672
+ },
673
+ functions,
674
+ kind: "Module",
675
+ location
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Invokes Babel and normalizes syntax failures.
681
+ * @param {object} input - Parser input.
682
+ * @param {string} input.filename - Source filename.
683
+ * @param {"javascript" | "typescript"} input.language - Frontend language.
684
+ * @param {string} input.source - Source text.
685
+ * @returns {import("@babel/parser").ParseResult<import("@babel/types").File>} Babel file.
686
+ */
687
+ function parseBabelSource({filename, language, source}) {
688
+ try {
689
+ return parseBabel(source, {
690
+ plugins: language == "typescript" ? ["typescript"] : [],
691
+ sourceFilename: filename,
692
+ sourceType: "script",
693
+ tokens: true
694
+ })
695
+ } catch (error) {
696
+ return parseFailure(language, error)
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Associates Babel nodes with their parser token stream without leaking it into semantic values.
702
+ * @param {object} value - Current parser value.
703
+ * @param {BabelTokenIndex} tokens - Indexed parser tokens.
704
+ * @param {WeakSet<object>} [visited] - Cycle protection.
705
+ * @returns {void}
706
+ */
707
+ function rememberTokens(value, tokens, visited = new WeakSet()) {
708
+ if (visited.has(value)) return
709
+
710
+ visited.add(value)
711
+ nodeTokens.set(value, tokens)
712
+
713
+ for (const child of Object.values(value)) {
714
+ if (Array.isArray(child)) {
715
+ for (const item of child) if (item && typeof item == "object") rememberTokens(item, tokens, visited)
716
+ } else if (child && typeof child == "object") rememberTokens(child, tokens, visited)
717
+ }
718
+ }