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,622 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ AndNode,
5
+ CallNode,
6
+ DefNode,
7
+ ElseNode,
8
+ FalseNode,
9
+ IfNode,
10
+ IntegerNode,
11
+ InterpolatedStringNode,
12
+ LocalVariableReadNode,
13
+ LocalVariableWriteNode,
14
+ OrNode,
15
+ ParenthesesNode,
16
+ ProgramNode,
17
+ RequiredParameterNode,
18
+ ReturnNode,
19
+ StatementsNode,
20
+ StringNode,
21
+ TrueNode,
22
+ loadPrism
23
+ } from "@ruby/prism"
24
+ import {missingType, SemantifoldDiagnostic, unsupportedSyntax} from "../diagnostic.js"
25
+ import {locationFromOffsets, moduleLocation, utf8ByteOffsetToUtf16Offset} from "../semantic/location.js"
26
+ import {withAdaptedOperation} from "../semantic/operators.js"
27
+ import {withParserRanges} from "../semantic/provenance.js"
28
+ import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
29
+ import {requireSourceScalarType} from "./scalars.js"
30
+ const parsePrism = await loadPrism()
31
+ const rubyBinaryOperations = new Map([
32
+ ["+", "Add"],
33
+ ["-", "Subtract"],
34
+ ["*", "Multiply"],
35
+ ["==", "Equal"],
36
+ ["!=", "NotEqual"],
37
+ ["<", "LessThan"],
38
+ ["<=", "LessThanOrEqual"],
39
+ [">", "GreaterThan"],
40
+ [">=", "GreaterThanOrEqual"]
41
+ ])
42
+
43
+ /**
44
+ * Returns a normalized Prism node location.
45
+ * @param {{location: import("@ruby/prism").Location}} node - Prism node.
46
+ * @param {string} filename - Source filename.
47
+ * @param {string} source - Complete source.
48
+ * @returns {import("../semantic/types.js").SourceLocation} Source location.
49
+ */
50
+ function nodeLocation(node, filename, source) {
51
+ return prismLocation(node.location, filename, source)
52
+ }
53
+
54
+ /**
55
+ * Converts a Prism location into a normalized source location.
56
+ * @param {import("@ruby/prism").Location} location - Prism location.
57
+ * @param {string} filename - Source filename.
58
+ * @param {string} source - Complete source.
59
+ * @returns {import("../semantic/types.js").SourceLocation} Source location.
60
+ */
61
+ function prismLocation(location, filename, source) {
62
+ return locationFromOffsets(
63
+ filename,
64
+ source,
65
+ utf8ByteOffsetToUtf16Offset(source, location.startOffset),
66
+ utf8ByteOffsetToUtf16Offset(source, location.startOffset + location.length)
67
+ )
68
+ }
69
+
70
+ /**
71
+ * Slices JavaScript source using Prism's UTF-8 byte offsets.
72
+ * @param {string} source - Complete source.
73
+ * @param {number} startOffset - Inclusive Prism byte offset.
74
+ * @param {number} endOffset - Exclusive Prism byte offset.
75
+ * @returns {string} Source text in the byte range.
76
+ */
77
+ function slicePrismSource(source, startOffset, endOffset) {
78
+ return source.slice(
79
+ utf8ByteOffsetToUtf16Offset(source, startOffset),
80
+ utf8ByteOffsetToUtf16Offset(source, endOffset)
81
+ )
82
+ }
83
+
84
+ /**
85
+ * Converts a supported Prism expression.
86
+ * @param {import("@ruby/prism").Node} node - Prism node.
87
+ * @param {string} filename - Source filename.
88
+ * @param {string} source - Complete source.
89
+ * @returns {import("../semantic/types.js").Expression} Semantic expression.
90
+ */
91
+ function convertExpression(node, filename, source) {
92
+ const location = nodeLocation(node, filename, source)
93
+
94
+ if (node instanceof ParenthesesNode && node.body instanceof StatementsNode && node.body.body.length == 1) {
95
+ return convertExpression(node.body.body[0], filename, source)
96
+ }
97
+
98
+ if (node instanceof LocalVariableReadNode) {
99
+ return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: node.name}, {name: location})
100
+ }
101
+
102
+ if (node instanceof IntegerNode) {
103
+ if (!Number.isSafeInteger(node.value)) return unsupportedSyntax("ruby", "non-safe integer literal", location)
104
+
105
+ return withParserRanges({kind: /** @type {const} */ ("IntegerLiteral"), location, value: node.value}, {literal: location})
106
+ }
107
+
108
+ if (node instanceof TrueNode || node instanceof FalseNode) {
109
+ return withParserRanges({kind: /** @type {const} */ ("BooleanLiteral"), location, value: node instanceof TrueNode}, {literal: location})
110
+ }
111
+
112
+ if (node instanceof StringNode) {
113
+ const decoded = node.unescaped
114
+
115
+ if (!decoded.validEncoding || decoded.encoding != "utf-8" || node.isForcedBinaryEncoding() ||
116
+ !hasOnlyUnicodeScalars(decoded.value)) {
117
+ return unsupportedSyntax("ruby", "invalid Unicode string literal", location)
118
+ }
119
+
120
+ return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: decoded.value}, {literal: location})
121
+ }
122
+
123
+ if (node instanceof InterpolatedStringNode) return unsupportedSyntax("ruby", "interpolated string", location)
124
+
125
+ if (node instanceof CallNode && node.receiver && node.name == "!") {
126
+ const operatorLocation = prismLocation(node.messageLoc ?? node.location, filename, source)
127
+
128
+ if (node.callOperatorLoc) return unsupportedSyntax("ruby", "qualified boolean not", operatorLocation)
129
+ if (node.messageLoc?.length != 1) {
130
+ return unsupportedSyntax("ruby", "precedence-sensitive boolean not", operatorLocation)
131
+ }
132
+ if (node.arguments_) return unsupportedSyntax("ruby", "boolean not argument list", operatorLocation)
133
+
134
+ const semantic = withAdaptedOperation(withParserRanges({
135
+ kind: "UnaryExpression",
136
+ location,
137
+ operand: convertExpression(node.receiver, filename, source)
138
+ }, {operator: operatorLocation}), "Not")
139
+
140
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
141
+ }
142
+
143
+ if (node instanceof CallNode && node.receiver && node.name == "-@") {
144
+ const operatorLocation = prismLocation(node.messageLoc ?? node.location, filename, source)
145
+
146
+ if (node.callOperatorLoc) return unsupportedSyntax("ruby", "qualified integer negation", operatorLocation)
147
+ if (node.arguments_) return unsupportedSyntax("ruby", "integer negation argument list", operatorLocation)
148
+
149
+ const semantic = withAdaptedOperation(withParserRanges({
150
+ kind: "UnaryExpression",
151
+ location,
152
+ operand: convertExpression(node.receiver, filename, source)
153
+ }, {operator: operatorLocation}), "Negate")
154
+
155
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
156
+ }
157
+
158
+ if (node instanceof AndNode || node instanceof OrNode) {
159
+ const operatorLocation = prismLocation(node.operatorLoc, filename, source)
160
+ const sourceOperator = source.slice(operatorLocation.start.offset, operatorLocation.end.offset)
161
+
162
+ if (sourceOperator != "&&" && sourceOperator != "||") {
163
+ return unsupportedSyntax("ruby", `precedence-sensitive boolean ${sourceOperator}`, location)
164
+ }
165
+
166
+ const semantic = withAdaptedOperation(withParserRanges({
167
+ kind: "BinaryExpression",
168
+ left: convertExpression(node.left, filename, source),
169
+ location,
170
+ right: convertExpression(node.right, filename, source)
171
+ }, {operator: operatorLocation}), node instanceof AndNode ? "And" : "Or")
172
+
173
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
174
+ }
175
+
176
+ if (node instanceof CallNode && node.receiver && rubyBinaryOperations.has(node.name) && node.callOperatorLoc &&
177
+ node.arguments_?.arguments_.length == 1) {
178
+ return unsupportedSyntax(
179
+ "ruby",
180
+ `qualified binary operator ${node.name}`,
181
+ prismLocation(node.messageLoc ?? node.location, filename, source)
182
+ )
183
+ }
184
+
185
+ if (node instanceof CallNode && node.receiver && rubyBinaryOperations.has(node.name) && node.arguments_?.arguments_.length == 1) {
186
+ const semantic = withAdaptedOperation(withParserRanges({
187
+ kind: "BinaryExpression",
188
+ left: convertExpression(node.receiver, filename, source),
189
+ location,
190
+ right: convertExpression(node.arguments_.arguments_[0], filename, source)
191
+ }, {operator: prismLocation(node.messageLoc ?? node.location, filename, source)}), /** @type {import("../semantic/operators.js").AdaptedOperation} */ (rubyBinaryOperations.get(node.name)))
192
+
193
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
194
+ }
195
+
196
+ if (node instanceof CallNode && !node.receiver) {
197
+ return withParserRanges({
198
+ arguments: (node.arguments_?.arguments_ ?? []).map((argument) => convertExpression(argument, filename, source)),
199
+ callee: node.name,
200
+ kind: "CallExpression",
201
+ location
202
+ }, {callee: prismLocation(node.messageLoc ?? node.location, filename, source)})
203
+ }
204
+
205
+ return unsupportedSyntax("ruby", node.constructor.name, location)
206
+ }
207
+
208
+ /**
209
+ * Converts one explicit Ruby return.
210
+ * @param {import("@ruby/prism").Node} node - Prism node.
211
+ * @param {string} filename - Source filename.
212
+ * @param {string} source - Complete source.
213
+ * @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
214
+ */
215
+ function convertReturn(node, filename, source) {
216
+ const location = nodeLocation(node, filename, source)
217
+
218
+ if (!(node instanceof ReturnNode) || node.arguments_?.arguments_.length != 1) {
219
+ return unsupportedSyntax("ruby", node.constructor.name, location)
220
+ }
221
+
222
+ return {
223
+ expression: convertExpression(node.arguments_.arguments_[0], filename, source),
224
+ kind: "ReturnStatement",
225
+ location
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Reads Prism-owned RBS-style type comments directly preceding a definition.
231
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
232
+ * @param {{location: import("@ruby/prism").Location}} node - Comment owner.
233
+ * @param {string} filename - Source filename.
234
+ * @param {string} source - Complete source.
235
+ * @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined}} Declared types.
236
+ */
237
+ function typeComments(comments, node, filename, source) {
238
+ const preceding = associatedComments(comments, node, source)
239
+ const parameterTypes = new Map()
240
+ /** @type {{location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined} */
241
+ let returnType
242
+
243
+ for (const comment of preceding) {
244
+ const words = commentTokens(comment, filename, source)
245
+
246
+ if (words[0]?.text == "@param" && words.length == 3) {
247
+ parameterTypes.set(words[1].text, {location: words[2].location, sourceType: words[2].text})
248
+ }
249
+ if (words[0]?.text == "@return" && words.length == 2) {
250
+ returnType = {location: words[1].location, sourceType: words[1].text}
251
+ }
252
+ }
253
+
254
+ return {parameters: parameterTypes, returnType}
255
+ }
256
+
257
+ /**
258
+ * Lexes whitespace-separated metadata tokens inside a Prism-owned Ruby comment.
259
+ * @param {import("@ruby/prism/src/deserialize.js").Comment} comment - Prism comment token.
260
+ * @param {string} filename - Source filename.
261
+ * @param {string} source - Complete source.
262
+ * @returns {{location: import("../semantic/types.js").SourceLocation, text: string}[]} Metadata tokens.
263
+ */
264
+ function commentTokens(comment, filename, source) {
265
+ const start = utf8ByteOffsetToUtf16Offset(source, comment.location.startOffset)
266
+ const end = utf8ByteOffsetToUtf16Offset(source, comment.location.startOffset + comment.location.length)
267
+ const tokens = []
268
+ let offset = start + 1
269
+
270
+ while (offset < end) {
271
+ while (offset < end && isRubyMetadataWhitespace(source[offset])) offset++
272
+ const tokenStart = offset
273
+
274
+ while (offset < end && !isRubyMetadataWhitespace(source[offset])) offset++
275
+ if (tokenStart < offset) {
276
+ tokens.push({location: locationFromOffsets(filename, source, tokenStart, offset), text: source.slice(tokenStart, offset)})
277
+ }
278
+ }
279
+
280
+ return tokens
281
+ }
282
+
283
+ /**
284
+ * Checks one Ruby metadata separator.
285
+ * @param {string} character - Source character.
286
+ * @returns {boolean} Whether it separates metadata tokens.
287
+ */
288
+ function isRubyMetadataWhitespace(character) {
289
+ return character == " " || character == "\t" || character == "\r" || character == "\n"
290
+ }
291
+
292
+ /**
293
+ * Selects only the contiguous comment block immediately before a definition.
294
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
295
+ * @param {{location: import("@ruby/prism").Location}} node - Comment owner.
296
+ * @param {string} source - Complete source.
297
+ * @returns {import("@ruby/prism/src/deserialize.js").Comment[]} Associated comments.
298
+ */
299
+ function associatedComments(comments, node, source) {
300
+ const preceding = comments.filter((comment) => comment.location.startOffset < node.location.startOffset)
301
+ .sort((left, right) => left.location.startOffset - right.location.startOffset)
302
+ const associated = []
303
+ let boundary = node.location.startOffset
304
+
305
+ for (let index = preceding.length - 1; index >= 0; index--) {
306
+ const comment = preceding[index]
307
+ const endOffset = comment.location.startOffset + comment.location.length
308
+ const gap = slicePrismSource(source, endOffset, boundary)
309
+
310
+ if (!isImmediateCommentGap(gap)) break
311
+
312
+ associated.unshift(comment)
313
+ boundary = comment.location.startOffset
314
+ }
315
+
316
+ return associated
317
+ }
318
+
319
+ /**
320
+ * Reads one exact Ruby local type carrier and immutability marker.
321
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
322
+ * @param {LocalVariableWriteNode} node - Local write.
323
+ * @param {string} filename - Source filename.
324
+ * @param {string} source - Complete source.
325
+ * @returns {{immutable: boolean, type: import("../semantic/types.js").TypeReference} | undefined} Metadata when present.
326
+ */
327
+ function localMetadata(comments, node, filename, source) {
328
+ const associated = associatedComments(comments, node, source)
329
+ const metadata = associated.map((comment) => commentTokens(comment, filename, source))
330
+ const typeMetadata = metadata.filter(([token]) => token?.text == "@type")
331
+ const immutableMetadata = metadata.filter(([token]) => token?.text == "@semantifold-immutable")
332
+ const profileMetadata = metadata.filter(([token]) => {
333
+ return token?.text.startsWith("@type") || token?.text.startsWith("@semantifold-immutable")
334
+ })
335
+
336
+ if (profileMetadata.length == 0) return undefined
337
+
338
+ const location = nodeLocation(node, filename, source)
339
+
340
+ if (typeMetadata.length != 1 || typeMetadata[0].length != 2 ||
341
+ immutableMetadata.length > 1 || immutableMetadata.some((tokens) => tokens.length != 1) ||
342
+ profileMetadata.length != typeMetadata.length + immutableMetadata.length) {
343
+ return unsupportedSyntax("ruby", "malformed local type metadata", location)
344
+ }
345
+
346
+ return {
347
+ immutable: immutableMetadata.length == 1,
348
+ type: convertType(typeMetadata[0][1].text, `Local '${node.name}'`, location, typeMetadata[0][1].location)
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Converts one Ruby local declaration or assignment.
354
+ * @param {import("@ruby/prism").Node} node - Prism node.
355
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
356
+ * @param {Set<string>} visible - Names visible during adaptation.
357
+ * @param {string} filename - Source filename.
358
+ * @param {string} source - Complete source.
359
+ * @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
360
+ */
361
+ function convertLocalStatement(node, comments, visible, filename, source) {
362
+ const location = nodeLocation(node, filename, source)
363
+
364
+ if (!(node instanceof LocalVariableWriteNode)) return unsupportedSyntax("ruby", node.constructor.name, location)
365
+
366
+ const metadata = localMetadata(comments, node, filename, source)
367
+
368
+ if (metadata) {
369
+ visible.add(node.name)
370
+ return withParserRanges({
371
+ initializer: convertExpression(node.value, filename, source),
372
+ kind: "LocalDeclaration",
373
+ location,
374
+ mutable: !metadata.immutable,
375
+ name: node.name,
376
+ type: metadata.type
377
+ }, {name: prismLocation(node.nameLoc, filename, source), operator: prismLocation(node.operatorLoc, filename, source)})
378
+ }
379
+
380
+ if (!visible.has(node.name)) return missingType("ruby", `Local '${node.name}'`, location)
381
+
382
+ const targetLocation = prismLocation(node.nameLoc, filename, source)
383
+ const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: node.name}, {
384
+ name: targetLocation
385
+ })
386
+
387
+ return withParserRanges({
388
+ expression: convertExpression(node.value, filename, source),
389
+ kind: "AssignmentStatement",
390
+ location,
391
+ target
392
+ }, {operator: prismLocation(node.operatorLoc, filename, source)})
393
+ }
394
+
395
+ /**
396
+ * Converts one exhaustive Prism statement.
397
+ * @param {import("@ruby/prism").Node} node - Prism statement.
398
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
399
+ * @param {Set<string>} visible - Names visible during adaptation.
400
+ * @param {string} filename - Source filename.
401
+ * @param {string} source - Complete source.
402
+ * @returns {import("../semantic/types.js").Statement} Semantic statement.
403
+ */
404
+ function convertStatement(node, comments, visible, filename, source) {
405
+ if (node instanceof ReturnNode) return convertReturn(node, filename, source)
406
+ if (node instanceof IfNode) return convertIf(node, comments, visible, filename, source)
407
+ if (node instanceof LocalVariableWriteNode) return convertLocalStatement(node, comments, visible, filename, source)
408
+ if (node instanceof CallNode) return convertPrint(node, filename, source)
409
+
410
+ return unsupportedSyntax("ruby", node.constructor.name, nodeLocation(node, filename, source))
411
+ }
412
+
413
+ /**
414
+ * Converts one ordered Prism statement list.
415
+ * @param {StatementsNode | null} node - Prism statements, or an empty source block.
416
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
417
+ * @param {Set<string>} visible - Names visible on block entry.
418
+ * @param {string} filename - Source filename.
419
+ * @param {string} source - Complete source.
420
+ * @param {import("../semantic/types.js").SourceLocation} fallbackLocation - Empty block location.
421
+ * @returns {import("../semantic/types.js").Block} Semantic block.
422
+ */
423
+ function convertBlock(node, comments, visible, filename, source, fallbackLocation) {
424
+ return {
425
+ kind: "Block",
426
+ location: node ? nodeLocation(node, filename, source) : fallbackLocation,
427
+ statements: node ? node.body.map((statement) => convertStatement(statement, comments, visible, filename, source)) : []
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Checks that a comment and its owner are separated by at most one newline.
433
+ * @param {string} gap - Source between the comment and following item.
434
+ * @returns {boolean} Whether the gap is immediate whitespace.
435
+ */
436
+ function isImmediateCommentGap(gap) {
437
+ let newlines = 0
438
+
439
+ for (const character of gap) {
440
+ if (character == "\n") {
441
+ newlines++
442
+ } else if (character != " " && character != "\t" && character != "\r") {
443
+ return false
444
+ }
445
+ }
446
+
447
+ return newlines <= 1
448
+ }
449
+
450
+ /**
451
+ * Requires an exact supported Ruby scalar spelling.
452
+ * @param {string | undefined} sourceType - Ruby type comment value.
453
+ * @param {string} subject - Typed subject.
454
+ * @param {import("../semantic/types.js").SourceLocation} location - Source location.
455
+ * @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type comment token location.
456
+ * @returns {import("../semantic/types.js").TypeReference} Semantic type.
457
+ */
458
+ function convertType(sourceType, subject, location, typeLocation = location) {
459
+ return requireSourceScalarType("ruby", sourceType, subject, location, typeLocation)
460
+ }
461
+
462
+ /**
463
+ * Converts a Ruby definition.
464
+ * @param {DefNode} node - Prism definition.
465
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
466
+ * @param {string} filename - Source filename.
467
+ * @param {string} source - Complete source.
468
+ * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
469
+ */
470
+ function convertFunction(node, comments, filename, source) {
471
+ const location = nodeLocation(node, filename, source)
472
+
473
+ if (!node.parameters || node.parameters.optionals.length > 0 || node.parameters.rest || node.parameters.posts.length > 0 ||
474
+ node.parameters.keywords.length > 0 || node.parameters.keywordRest || node.parameters.block ||
475
+ node.parameters.requireds.some((parameter) => !(parameter instanceof RequiredParameterNode))) {
476
+ return unsupportedSyntax("ruby", "parameters", location)
477
+ }
478
+
479
+ const declaredTypes = typeComments(comments, node, filename, source)
480
+ const parameters = node.parameters.requireds.map((parameter) => {
481
+ if (!(parameter instanceof RequiredParameterNode)) return unsupportedSyntax("ruby", parameter.constructor.name, location)
482
+
483
+ const parameterLocation = nodeLocation(parameter, filename, source)
484
+
485
+ const declaredType = declaredTypes.parameters.get(parameter.name)
486
+
487
+ return withParserRanges({
488
+ kind: /** @type {const} */ ("Parameter"),
489
+ location: parameterLocation,
490
+ name: parameter.name,
491
+ type: convertType(declaredType?.sourceType, `Parameter '${parameter.name}'`, parameterLocation, declaredType?.location)
492
+ }, {name: parameterLocation})
493
+ })
494
+ const visible = new Set(parameters.map((parameter) => parameter.name))
495
+ const body = convertBlock(node.body instanceof StatementsNode ? node.body : null, comments, visible, filename, source, location)
496
+
497
+ return withParserRanges({
498
+ body,
499
+ kind: "FunctionDeclaration",
500
+ location,
501
+ name: node.name,
502
+ parameters,
503
+ returnType: convertType(declaredTypes.returnType?.sourceType, `Function '${node.name}' return`, location, declaredTypes.returnType?.location)
504
+ }, {name: prismLocation(node.nameLoc, filename, source)})
505
+ }
506
+
507
+ /**
508
+ * Converts the existing Ruby if/else terminal with restricted branch prefixes.
509
+ * @param {IfNode} node - Prism if node.
510
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
511
+ * @param {Set<string>} visible - Enclosing visible names.
512
+ * @param {string} filename - Source filename.
513
+ * @param {string} source - Complete source.
514
+ * @returns {import("../semantic/types.js").IfStatement} Semantic branch.
515
+ */
516
+ function convertIf(node, comments, visible, filename, source) {
517
+ const location = nodeLocation(node, filename, source)
518
+
519
+ if (!node.ifKeywordLoc || node.ifKeywordLoc.startOffset != node.location.startOffset) {
520
+ return unsupportedSyntax("ruby", "modifier conditional", node.ifKeywordLoc
521
+ ? prismLocation(node.ifKeywordLoc, filename, source)
522
+ : location)
523
+ }
524
+
525
+ const consequentVisible = new Set(visible)
526
+ let alternate
527
+
528
+ if (node.subsequent) {
529
+ const alternateVisible = new Set(visible)
530
+
531
+ if (node.subsequent instanceof ElseNode) {
532
+ alternate = convertBlock(node.subsequent.statements, comments, alternateVisible, filename, source,
533
+ nodeLocation(node.subsequent, filename, source))
534
+ } else if (node.subsequent instanceof IfNode) {
535
+ alternate = {
536
+ kind: /** @type {const} */ ("Block"),
537
+ location: nodeLocation(node.subsequent, filename, source),
538
+ statements: [convertIf(node.subsequent, comments, alternateVisible, filename, source)]
539
+ }
540
+ } else return unsupportedSyntax("ruby", node.subsequent.constructor.name, nodeLocation(node.subsequent, filename, source))
541
+ }
542
+
543
+ return {
544
+ ...(alternate ? {alternate} : {}),
545
+ condition: convertExpression(node.predicate, filename, source),
546
+ consequent: convertBlock(node.statements, comments, consequentVisible, filename, source, location),
547
+ kind: "IfStatement",
548
+ location
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Converts the supported Ruby puts entry point.
554
+ * @param {import("@ruby/prism").Node} node - Prism node.
555
+ * @param {string} filename - Source filename.
556
+ * @param {string} source - Complete source.
557
+ * @returns {import("../semantic/types.js").PrintStatement} Print statement.
558
+ */
559
+ function convertPrint(node, filename, source) {
560
+ const location = nodeLocation(node, filename, source)
561
+
562
+ if (!(node instanceof CallNode) || node.receiver || node.name != "puts" || node.arguments_?.arguments_.length != 1) {
563
+ return unsupportedSyntax("ruby", node.constructor.name, location)
564
+ }
565
+
566
+ return {expression: convertExpression(node.arguments_.arguments_[0], filename, source), kind: "PrintStatement", location}
567
+ }
568
+
569
+ /**
570
+ * Parses Ruby into the shared semantic module.
571
+ * @param {object} input - Parser input.
572
+ * @param {string} input.filename - Source filename.
573
+ * @param {string} input.source - Source text.
574
+ * @returns {import("../semantic/types.js").SemanticModule} Semantic module.
575
+ */
576
+ export function parseRuby({filename, source}) {
577
+ const result = parsePrism(source, {filepath: filename})
578
+
579
+ if (result.errors.length > 0) {
580
+ const error = result.errors[0]
581
+ const location = locationFromOffsets(filename, source, error.location.startOffset, error.location.startOffset + error.location.length)
582
+
583
+ throw new SemantifoldDiagnostic({code: "PARSE_ERROR", language: "ruby", location, message: error.message})
584
+ }
585
+
586
+ if (!(result.value instanceof ProgramNode)) throw new Error("Prism returned a non-program root.")
587
+
588
+ const body = result.value.statements.body
589
+ const functions = body.filter((node) => node instanceof DefNode)
590
+ .map((node) => convertFunction(node, result.comments, filename, source))
591
+ const entryNodes = body.filter((node) => !(node instanceof DefNode))
592
+ const location = moduleLocation(filename, source)
593
+
594
+ if (functions.length == 0) return unsupportedSyntax("ruby", "module without a function", location)
595
+ const firstEntry = entryNodes[0]
596
+ const entryLocation = firstEntry ? locationFromOffsets(
597
+ filename,
598
+ source,
599
+ utf8ByteOffsetToUtf16Offset(source, firstEntry.location.startOffset),
600
+ utf8ByteOffsetToUtf16Offset(
601
+ source,
602
+ (entryNodes.at(-1)?.location.startOffset ?? 0) + (entryNodes.at(-1)?.location.length ?? 0)
603
+ )
604
+ ) : location
605
+ const entryVisible = new Set()
606
+ const entryBlock = {
607
+ kind: /** @type {const} */ ("Block"),
608
+ location: entryLocation,
609
+ statements: entryNodes.map((statement) => convertStatement(statement, result.comments, entryVisible, filename, source))
610
+ }
611
+
612
+ return {
613
+ entryPoint: {
614
+ body: entryBlock,
615
+ kind: "EntryPoint",
616
+ location: entryLocation
617
+ },
618
+ functions,
619
+ kind: "Module",
620
+ location
621
+ }
622
+ }