konpeito 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 (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,1214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "stringio"
5
+
6
+ module Konpeito
7
+ module Formatter
8
+ class Formatter
9
+ INDENT_SIZE = 2
10
+
11
+ def initialize(source, filepath: nil)
12
+ @source = source
13
+ @filepath = filepath
14
+ @result = Prism.parse(source, filepath: filepath)
15
+ @comments = @result.comments.dup
16
+ @output = StringIO.new
17
+ @indent = 0
18
+ @line_started = false
19
+ @last_was_newline = false
20
+ @current_line = 1
21
+ end
22
+
23
+ def format
24
+ visit(@result.value)
25
+ flush_remaining_comments
26
+ result = @output.string
27
+ # Ensure trailing newline
28
+ result += "\n" unless result.end_with?("\n")
29
+ # Collapse 3+ consecutive newlines into 2
30
+ result.gsub(/\n{3,}/, "\n\n")
31
+ end
32
+
33
+ private
34
+
35
+ # --- Core emit helpers ---
36
+
37
+ def emit(str)
38
+ return if str.nil? || str.empty?
39
+ if !@line_started
40
+ @output << (" " * (INDENT_SIZE * @indent))
41
+ @line_started = true
42
+ end
43
+ @output << str
44
+ @last_was_newline = false
45
+ end
46
+
47
+ def emit_newline
48
+ @output << "\n"
49
+ @line_started = false
50
+ @last_was_newline = true
51
+ @current_line += 1
52
+ end
53
+
54
+ def emit_blank_line
55
+ # Ensure we end current line if needed, then add one blank line
56
+ unless @last_was_newline
57
+ emit_newline
58
+ end
59
+ @output << "\n"
60
+ @current_line += 1
61
+ end
62
+
63
+ def indent
64
+ @indent += 1
65
+ end
66
+
67
+ def dedent
68
+ @indent -= 1
69
+ @indent = 0 if @indent < 0
70
+ end
71
+
72
+ # --- Comment handling ---
73
+
74
+ def emit_leading_comments(node)
75
+ return unless node.respond_to?(:location)
76
+ target_line = node.location.start_line
77
+ last_comment_line = nil
78
+
79
+ while (comment = @comments.first) && comment.location.start_line < target_line
80
+ @comments.shift
81
+
82
+ # Preserve blank line between comments/before this comment
83
+ if last_comment_line && comment.location.start_line - last_comment_line > 1
84
+ emit_blank_line
85
+ end
86
+
87
+ emit(comment.location.slice)
88
+ emit_newline
89
+ last_comment_line = comment.location.end_line
90
+ end
91
+
92
+ # Preserve blank line between last comment and the node
93
+ if last_comment_line && node.location.start_line - last_comment_line > 1
94
+ emit_blank_line
95
+ end
96
+ end
97
+
98
+ def emit_inline_comment(node)
99
+ return unless node.respond_to?(:location)
100
+ end_line = node.location.end_line
101
+
102
+ if (comment = @comments.first) && comment.location.start_line == end_line
103
+ @comments.shift
104
+ emit(" ")
105
+ emit(comment.location.slice)
106
+ end
107
+ end
108
+
109
+ def flush_remaining_comments
110
+ @comments.each do |comment|
111
+ emit(comment.location.slice)
112
+ emit_newline
113
+ end
114
+ @comments.clear
115
+ end
116
+
117
+ # --- Visit dispatch ---
118
+
119
+ def visit(node)
120
+ return unless node
121
+
122
+ method_name = :"visit_#{node.type}"
123
+ if respond_to?(method_name, true)
124
+ send(method_name, node)
125
+ else
126
+ # Fallback: emit original source
127
+ emit(node.location.slice)
128
+ end
129
+ end
130
+
131
+ def visit_list(nodes, separator: nil)
132
+ nodes.each_with_index do |node, i|
133
+ visit(node)
134
+ if separator && i < nodes.size - 1
135
+ emit(separator)
136
+ end
137
+ end
138
+ end
139
+
140
+ # --- Node visitors ---
141
+
142
+ def visit_program_node(node)
143
+ visit(node.statements)
144
+ end
145
+
146
+ def visit_statements_node(node)
147
+ body = node.body
148
+ body.each_with_index do |stmt, i|
149
+ emit_leading_comments(stmt)
150
+ visit(stmt)
151
+ emit_inline_comment(stmt)
152
+ emit_newline unless i == body.size - 1 && @last_was_newline
153
+
154
+ # Preserve blank lines between statements based on original source
155
+ if i < body.size - 1
156
+ next_stmt = body[i + 1]
157
+ current_end = stmt.location.end_line
158
+ # Check for leading comments on next node
159
+ next_start = next_stmt.location.start_line
160
+ if @comments.first && @comments.first.location.start_line < next_start
161
+ next_start = @comments.first.location.start_line
162
+ end
163
+
164
+ if next_start - current_end > 1 || needs_blank_line_between?(stmt, next_stmt)
165
+ emit_blank_line
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def needs_blank_line_between?(a, b)
172
+ [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].any? { |k| a.is_a?(k) || b.is_a?(k) }
173
+ end
174
+
175
+ # --- Literals ---
176
+
177
+ def visit_integer_node(node)
178
+ emit(node.location.slice)
179
+ end
180
+
181
+ def visit_float_node(node)
182
+ emit(node.location.slice)
183
+ end
184
+
185
+ def visit_rational_node(node)
186
+ emit(node.location.slice)
187
+ end
188
+
189
+ def visit_imaginary_node(node)
190
+ emit(node.location.slice)
191
+ end
192
+
193
+ def visit_string_node(node)
194
+ emit(node.location.slice)
195
+ end
196
+
197
+ def visit_interpolated_string_node(node)
198
+ emit(node.location.slice)
199
+ end
200
+
201
+ def visit_symbol_node(node)
202
+ emit(node.location.slice)
203
+ end
204
+
205
+ def visit_interpolated_symbol_node(node)
206
+ emit(node.location.slice)
207
+ end
208
+
209
+ def visit_regular_expression_node(node)
210
+ emit(node.location.slice)
211
+ end
212
+
213
+ def visit_interpolated_regular_expression_node(node)
214
+ emit(node.location.slice)
215
+ end
216
+
217
+ def visit_x_string_node(node)
218
+ emit(node.location.slice)
219
+ end
220
+
221
+ def visit_true_node(node)
222
+ emit("true")
223
+ end
224
+
225
+ def visit_false_node(node)
226
+ emit("false")
227
+ end
228
+
229
+ def visit_nil_node(node)
230
+ emit("nil")
231
+ end
232
+
233
+ def visit_self_node(node)
234
+ emit("self")
235
+ end
236
+
237
+ def visit_source_file_node(node)
238
+ emit("__FILE__")
239
+ end
240
+
241
+ def visit_source_line_node(node)
242
+ emit("__LINE__")
243
+ end
244
+
245
+ def visit_source_encoding_node(node)
246
+ emit("__ENCODING__")
247
+ end
248
+
249
+ # --- Variables ---
250
+
251
+ def visit_local_variable_read_node(node)
252
+ emit(node.name.to_s)
253
+ end
254
+
255
+ def visit_local_variable_write_node(node)
256
+ emit(node.name.to_s)
257
+ emit(" = ")
258
+ visit(node.value)
259
+ end
260
+
261
+ def visit_local_variable_operator_write_node(node)
262
+ emit(node.name.to_s)
263
+ emit(" #{node.binary_operator}= ")
264
+ visit(node.value)
265
+ end
266
+
267
+ def visit_local_variable_and_write_node(node)
268
+ emit(node.name.to_s)
269
+ emit(" &&= ")
270
+ visit(node.value)
271
+ end
272
+
273
+ def visit_local_variable_or_write_node(node)
274
+ emit(node.name.to_s)
275
+ emit(" ||= ")
276
+ visit(node.value)
277
+ end
278
+
279
+ def visit_local_variable_target_node(node)
280
+ emit(node.name.to_s)
281
+ end
282
+
283
+ def visit_instance_variable_read_node(node)
284
+ emit(node.name.to_s)
285
+ end
286
+
287
+ def visit_instance_variable_write_node(node)
288
+ emit(node.name.to_s)
289
+ emit(" = ")
290
+ visit(node.value)
291
+ end
292
+
293
+ def visit_instance_variable_operator_write_node(node)
294
+ emit(node.name.to_s)
295
+ emit(" #{node.binary_operator}= ")
296
+ visit(node.value)
297
+ end
298
+
299
+ def visit_instance_variable_and_write_node(node)
300
+ emit(node.name.to_s)
301
+ emit(" &&= ")
302
+ visit(node.value)
303
+ end
304
+
305
+ def visit_instance_variable_or_write_node(node)
306
+ emit(node.name.to_s)
307
+ emit(" ||= ")
308
+ visit(node.value)
309
+ end
310
+
311
+ def visit_instance_variable_target_node(node)
312
+ emit(node.name.to_s)
313
+ end
314
+
315
+ def visit_class_variable_read_node(node)
316
+ emit(node.name.to_s)
317
+ end
318
+
319
+ def visit_class_variable_write_node(node)
320
+ emit(node.name.to_s)
321
+ emit(" = ")
322
+ visit(node.value)
323
+ end
324
+
325
+ def visit_class_variable_operator_write_node(node)
326
+ emit(node.name.to_s)
327
+ emit(" #{node.binary_operator}= ")
328
+ visit(node.value)
329
+ end
330
+
331
+ def visit_class_variable_and_write_node(node)
332
+ emit(node.name.to_s)
333
+ emit(" &&= ")
334
+ visit(node.value)
335
+ end
336
+
337
+ def visit_class_variable_or_write_node(node)
338
+ emit(node.name.to_s)
339
+ emit(" ||= ")
340
+ visit(node.value)
341
+ end
342
+
343
+ def visit_global_variable_read_node(node)
344
+ emit(node.name.to_s)
345
+ end
346
+
347
+ def visit_global_variable_write_node(node)
348
+ emit(node.name.to_s)
349
+ emit(" = ")
350
+ visit(node.value)
351
+ end
352
+
353
+ def visit_constant_read_node(node)
354
+ emit(node.name.to_s)
355
+ end
356
+
357
+ def visit_constant_write_node(node)
358
+ emit(node.name.to_s)
359
+ emit(" = ")
360
+ visit(node.value)
361
+ end
362
+
363
+ def visit_constant_path_node(node)
364
+ if node.parent
365
+ visit(node.parent)
366
+ emit("::")
367
+ else
368
+ emit("::")
369
+ end
370
+ emit(node.name.to_s)
371
+ end
372
+
373
+ def visit_constant_path_write_node(node)
374
+ visit(node.target)
375
+ emit(" = ")
376
+ visit(node.value)
377
+ end
378
+
379
+ # --- Method definitions ---
380
+
381
+ def visit_def_node(node)
382
+ emit("def ")
383
+ if node.receiver
384
+ visit(node.receiver)
385
+ emit(".")
386
+ end
387
+ emit(node.name.to_s)
388
+
389
+ if node.parameters
390
+ emit("(")
391
+ visit(node.parameters)
392
+ emit(")")
393
+ end
394
+
395
+ if node.body
396
+ emit_newline
397
+ indent
398
+ visit(node.body)
399
+ emit_newline unless @last_was_newline
400
+ dedent
401
+ emit("end")
402
+ else
403
+ emit_newline
404
+ emit("end")
405
+ end
406
+ end
407
+
408
+ def visit_parameters_node(node)
409
+ params = []
410
+
411
+ node.requireds.each { |p| params << p }
412
+ node.optionals.each { |p| params << p }
413
+ params << node.rest if node.rest
414
+ node.posts.each { |p| params << p }
415
+ node.keywords.each { |p| params << p }
416
+ params << node.keyword_rest if node.keyword_rest
417
+ params << node.block if node.block
418
+
419
+ params.each_with_index do |param, i|
420
+ visit(param)
421
+ emit(", ") if i < params.size - 1
422
+ end
423
+ end
424
+
425
+ def visit_required_parameter_node(node)
426
+ emit(node.name.to_s)
427
+ end
428
+
429
+ def visit_optional_parameter_node(node)
430
+ emit(node.name.to_s)
431
+ emit(" = ")
432
+ visit(node.value)
433
+ end
434
+
435
+ def visit_rest_parameter_node(node)
436
+ emit("*")
437
+ emit(node.name.to_s) if node.name
438
+ end
439
+
440
+ def visit_keyword_rest_parameter_node(node)
441
+ emit("**")
442
+ emit(node.name.to_s) if node.name
443
+ end
444
+
445
+ def visit_required_keyword_parameter_node(node)
446
+ emit(node.name.to_s)
447
+ emit(":")
448
+ end
449
+
450
+ def visit_optional_keyword_parameter_node(node)
451
+ emit(node.name.to_s)
452
+ emit(": ")
453
+ visit(node.value)
454
+ end
455
+
456
+ def visit_block_parameter_node(node)
457
+ emit("&")
458
+ emit(node.name.to_s) if node.name
459
+ end
460
+
461
+ # --- Class / Module ---
462
+
463
+ def visit_class_node(node)
464
+ emit("class ")
465
+ visit(node.constant_path)
466
+ if node.superclass
467
+ emit(" < ")
468
+ visit(node.superclass)
469
+ end
470
+ emit_newline
471
+
472
+ if node.body
473
+ indent
474
+ visit(node.body)
475
+ emit_newline unless @last_was_newline
476
+ dedent
477
+ end
478
+
479
+ emit("end")
480
+ end
481
+
482
+ def visit_module_node(node)
483
+ emit("module ")
484
+ visit(node.constant_path)
485
+ emit_newline
486
+
487
+ if node.body
488
+ indent
489
+ visit(node.body)
490
+ emit_newline unless @last_was_newline
491
+ dedent
492
+ end
493
+
494
+ emit("end")
495
+ end
496
+
497
+ def visit_singleton_class_node(node)
498
+ emit("class << ")
499
+ visit(node.expression)
500
+ emit_newline
501
+
502
+ if node.body
503
+ indent
504
+ visit(node.body)
505
+ emit_newline unless @last_was_newline
506
+ dedent
507
+ end
508
+
509
+ emit("end")
510
+ end
511
+
512
+ # --- Control flow ---
513
+
514
+ def visit_if_node(node)
515
+ # Ternary or modifier form — fall back to source
516
+ if node.location.slice.include?("?") && !node.location.slice.strip.start_with?("if")
517
+ emit(node.location.slice)
518
+ return
519
+ end
520
+
521
+ # Check for modifier if (single-line "expr if cond")
522
+ src = node.location.slice.strip
523
+ if !src.start_with?("if") && !src.start_with?("elsif")
524
+ emit(node.location.slice)
525
+ return
526
+ end
527
+
528
+ if src.start_with?("elsif")
529
+ emit("elsif ")
530
+ else
531
+ emit("if ")
532
+ end
533
+ visit(node.predicate)
534
+ emit_newline
535
+
536
+ if node.statements
537
+ indent
538
+ visit(node.statements)
539
+ emit_newline unless @last_was_newline
540
+ dedent
541
+ end
542
+
543
+ if node.subsequent
544
+ visit_if_subsequent(node.subsequent)
545
+ end
546
+
547
+ # Only top-level if gets 'end', elsif doesn't
548
+ emit("end") unless src.start_with?("elsif")
549
+ end
550
+
551
+ def visit_if_subsequent(node)
552
+ case node
553
+ when Prism::IfNode
554
+ # elsif
555
+ emit("elsif ")
556
+ visit(node.predicate)
557
+ emit_newline
558
+ if node.statements
559
+ indent
560
+ visit(node.statements)
561
+ emit_newline unless @last_was_newline
562
+ dedent
563
+ end
564
+ visit_if_subsequent(node.subsequent) if node.subsequent
565
+ when Prism::ElseNode
566
+ visit_else_node(node)
567
+ end
568
+ end
569
+
570
+ def visit_else_node(node)
571
+ emit("else")
572
+ emit_newline
573
+ if node.statements
574
+ indent
575
+ visit(node.statements)
576
+ emit_newline unless @last_was_newline
577
+ dedent
578
+ end
579
+ end
580
+
581
+ def visit_unless_node(node)
582
+ emit("unless ")
583
+ visit(node.predicate)
584
+ emit_newline
585
+
586
+ if node.statements
587
+ indent
588
+ visit(node.statements)
589
+ emit_newline unless @last_was_newline
590
+ dedent
591
+ end
592
+
593
+ if node.else_clause
594
+ visit_else_node(node.else_clause)
595
+ end
596
+
597
+ emit("end")
598
+ end
599
+
600
+ def visit_while_node(node)
601
+ emit("while ")
602
+ visit(node.predicate)
603
+ emit_newline
604
+
605
+ if node.statements
606
+ indent
607
+ visit(node.statements)
608
+ emit_newline unless @last_was_newline
609
+ dedent
610
+ end
611
+
612
+ emit("end")
613
+ end
614
+
615
+ def visit_until_node(node)
616
+ emit("until ")
617
+ visit(node.predicate)
618
+ emit_newline
619
+
620
+ if node.statements
621
+ indent
622
+ visit(node.statements)
623
+ emit_newline unless @last_was_newline
624
+ dedent
625
+ end
626
+
627
+ emit("end")
628
+ end
629
+
630
+ def visit_for_node(node)
631
+ emit("for ")
632
+ visit(node.index)
633
+ emit(" in ")
634
+ visit(node.collection)
635
+ emit_newline
636
+
637
+ if node.statements
638
+ indent
639
+ visit(node.statements)
640
+ emit_newline unless @last_was_newline
641
+ dedent
642
+ end
643
+
644
+ emit("end")
645
+ end
646
+
647
+ def visit_case_node(node)
648
+ emit("case ")
649
+ visit(node.predicate) if node.predicate
650
+ emit_newline
651
+
652
+ node.conditions.each do |cond|
653
+ visit(cond)
654
+ end
655
+
656
+ if node.else_clause
657
+ visit_else_node(node.else_clause)
658
+ end
659
+
660
+ emit("end")
661
+ end
662
+
663
+ def visit_when_node(node)
664
+ emit("when ")
665
+ visit_list(node.conditions, separator: ", ")
666
+ emit_newline
667
+
668
+ if node.statements
669
+ indent
670
+ visit(node.statements)
671
+ emit_newline unless @last_was_newline
672
+ dedent
673
+ end
674
+ end
675
+
676
+ def visit_case_match_node(node)
677
+ emit("case ")
678
+ visit(node.predicate) if node.predicate
679
+ emit_newline
680
+
681
+ node.conditions.each do |cond|
682
+ visit(cond)
683
+ end
684
+
685
+ if node.else_clause
686
+ visit_else_node(node.else_clause)
687
+ end
688
+
689
+ emit("end")
690
+ end
691
+
692
+ def visit_in_node(node)
693
+ emit("in ")
694
+ visit(node.pattern)
695
+ if node.statements
696
+ emit_newline
697
+ indent
698
+ visit(node.statements)
699
+ emit_newline unless @last_was_newline
700
+ dedent
701
+ else
702
+ emit_newline
703
+ end
704
+ end
705
+
706
+ # --- Method calls ---
707
+
708
+ def visit_call_node(node)
709
+ # Handle special operator calls
710
+ name = node.name.to_s
711
+
712
+ if node.receiver
713
+ visit(node.receiver)
714
+
715
+ if name == "[]"
716
+ emit("[")
717
+ visit(node.arguments) if node.arguments
718
+ emit("]")
719
+ visit(node.block) if node.block
720
+ return
721
+ elsif name == "[]="
722
+ emit("[")
723
+ args = node.arguments.arguments
724
+ visit(args.first)
725
+ emit("] = ")
726
+ visit(args.last)
727
+ return
728
+ end
729
+
730
+ # Binary operators
731
+ if binary_operator?(name) && node.arguments && node.arguments.arguments.size == 1
732
+ emit(" #{name} ")
733
+ visit(node.arguments.arguments.first)
734
+ visit(node.block) if node.block
735
+ return
736
+ end
737
+
738
+ # Unary operators
739
+ if unary_operator?(name) && (!node.arguments || node.arguments.arguments.empty?)
740
+ emit(name)
741
+ return
742
+ end
743
+
744
+ # Safe navigation
745
+ if node.call_operator_loc && node.call_operator_loc.slice == "&."
746
+ emit("&.")
747
+ else
748
+ emit(".")
749
+ end
750
+ end
751
+
752
+ emit(name)
753
+
754
+ if node.opening_loc
755
+ # Explicit parentheses in source
756
+ emit("(")
757
+ visit(node.arguments) if node.arguments
758
+ emit(")")
759
+ elsif node.arguments
760
+ # No parentheses in source — preserve that style
761
+ emit(" ")
762
+ visit(node.arguments)
763
+ end
764
+
765
+ visit(node.block) if node.block
766
+ end
767
+
768
+ def binary_operator?(name)
769
+ %w[+ - * / % ** == != < > <= >= <=> << >> & | ^ =~ !~ === .. ...].include?(name)
770
+ end
771
+
772
+ def unary_operator?(name)
773
+ %w[-@ +@].include?(name)
774
+ end
775
+
776
+ def visit_arguments_node(node)
777
+ node.arguments.each_with_index do |arg, i|
778
+ visit(arg)
779
+ emit(", ") if i < node.arguments.size - 1
780
+ end
781
+ end
782
+
783
+ # --- Blocks ---
784
+
785
+ def visit_block_node(node)
786
+ # Determine brace vs do/end style
787
+ single_line = single_line_block?(node)
788
+
789
+ if single_line
790
+ emit(" { ")
791
+ if node.parameters
792
+ emit("|")
793
+ visit(node.parameters)
794
+ emit("| ")
795
+ end
796
+ if node.body.is_a?(Prism::StatementsNode)
797
+ visit_list(node.body.body, separator: "; ")
798
+ elsif node.body
799
+ visit(node.body)
800
+ end
801
+ emit(" }")
802
+ else
803
+ emit(" do")
804
+ if node.parameters
805
+ emit(" |")
806
+ visit(node.parameters)
807
+ emit("|")
808
+ end
809
+ emit_newline
810
+ if node.body
811
+ indent
812
+ visit(node.body)
813
+ emit_newline unless @last_was_newline
814
+ dedent
815
+ end
816
+ emit("end")
817
+ end
818
+ end
819
+
820
+ def single_line_block?(node)
821
+ return false unless node.body
822
+ # If the block fits on one line in source, keep it that way
823
+ node.location.start_line == node.location.end_line
824
+ end
825
+
826
+ def visit_block_parameters_node(node)
827
+ params = node.parameters
828
+ if params
829
+ visit(params)
830
+ end
831
+ end
832
+
833
+ def visit_lambda_node(node)
834
+ emit("->")
835
+ if node.parameters
836
+ emit("(")
837
+ visit(node.parameters)
838
+ emit(")")
839
+ end
840
+
841
+ if node.body && node.location.start_line == node.location.end_line
842
+ emit(" { ")
843
+ visit(node.body)
844
+ emit(" }")
845
+ else
846
+ emit(" do")
847
+ emit_newline
848
+ if node.body
849
+ indent
850
+ visit(node.body)
851
+ emit_newline unless @last_was_newline
852
+ dedent
853
+ end
854
+ emit("end")
855
+ end
856
+ end
857
+
858
+ # --- Exception handling ---
859
+
860
+ def visit_begin_node(node)
861
+ emit("begin")
862
+ emit_newline
863
+
864
+ if node.statements
865
+ indent
866
+ visit(node.statements)
867
+ emit_newline unless @last_was_newline
868
+ dedent
869
+ end
870
+
871
+ visit(node.rescue_clause) if node.rescue_clause
872
+
873
+ if node.else_clause
874
+ visit_else_node(node.else_clause)
875
+ end
876
+
877
+ if node.ensure_clause
878
+ visit(node.ensure_clause)
879
+ end
880
+
881
+ emit("end")
882
+ end
883
+
884
+ def visit_rescue_node(node)
885
+ emit("rescue")
886
+ if node.exceptions && !node.exceptions.empty?
887
+ emit(" ")
888
+ visit_list(node.exceptions, separator: ", ")
889
+ end
890
+ if node.reference
891
+ emit(" => ")
892
+ visit(node.reference)
893
+ end
894
+ emit_newline
895
+
896
+ if node.statements
897
+ indent
898
+ visit(node.statements)
899
+ emit_newline unless @last_was_newline
900
+ dedent
901
+ end
902
+
903
+ visit(node.subsequent) if node.subsequent
904
+ end
905
+
906
+ def visit_ensure_node(node)
907
+ emit("ensure")
908
+ emit_newline
909
+
910
+ if node.statements
911
+ indent
912
+ visit(node.statements)
913
+ emit_newline unless @last_was_newline
914
+ dedent
915
+ end
916
+ end
917
+
918
+ def visit_rescue_modifier_node(node)
919
+ visit(node.expression)
920
+ emit(" rescue ")
921
+ visit(node.rescue_expression)
922
+ end
923
+
924
+ # --- Return / Break / Next ---
925
+
926
+ def visit_return_node(node)
927
+ emit("return")
928
+ if node.arguments
929
+ emit(" ")
930
+ visit(node.arguments)
931
+ end
932
+ end
933
+
934
+ def visit_break_node(node)
935
+ emit("break")
936
+ if node.arguments
937
+ emit(" ")
938
+ visit(node.arguments)
939
+ end
940
+ end
941
+
942
+ def visit_next_node(node)
943
+ emit("next")
944
+ if node.arguments
945
+ emit(" ")
946
+ visit(node.arguments)
947
+ end
948
+ end
949
+
950
+ def visit_yield_node(node)
951
+ emit("yield")
952
+ if node.arguments
953
+ emit("(")
954
+ visit(node.arguments)
955
+ emit(")")
956
+ end
957
+ end
958
+
959
+ # --- Array / Hash ---
960
+
961
+ def visit_array_node(node)
962
+ # Detect %w / %i literals
963
+ src = node.location.slice
964
+ if src.start_with?("%w") || src.start_with?("%i") || src.start_with?("%W") || src.start_with?("%I")
965
+ emit(src)
966
+ return
967
+ end
968
+
969
+ emit("[")
970
+ node.elements.each_with_index do |elem, i|
971
+ visit(elem)
972
+ emit(", ") if i < node.elements.size - 1
973
+ end
974
+ emit("]")
975
+ end
976
+
977
+ def visit_hash_node(node)
978
+ emit("{")
979
+ unless node.elements.empty?
980
+ emit(" ")
981
+ node.elements.each_with_index do |elem, i|
982
+ visit(elem)
983
+ emit(", ") if i < node.elements.size - 1
984
+ end
985
+ emit(" ")
986
+ end
987
+ emit("}")
988
+ end
989
+
990
+ def visit_assoc_node(node)
991
+ visit(node.key)
992
+ # Symbol key with shorthand: { foo: bar } vs { "foo" => bar }
993
+ if node.key.is_a?(Prism::SymbolNode) && node.operator_loc.nil?
994
+ emit(" ")
995
+ else
996
+ emit(" => ")
997
+ end
998
+ visit(node.value)
999
+ end
1000
+
1001
+ def visit_assoc_splat_node(node)
1002
+ emit("**")
1003
+ visit(node.value) if node.value
1004
+ end
1005
+
1006
+ def visit_splat_node(node)
1007
+ emit("*")
1008
+ visit(node.expression) if node.expression
1009
+ end
1010
+
1011
+ # --- Range ---
1012
+
1013
+ def visit_range_node(node)
1014
+ visit(node.left) if node.left
1015
+ if node.exclude_end?
1016
+ emit("...")
1017
+ else
1018
+ emit("..")
1019
+ end
1020
+ visit(node.right) if node.right
1021
+ end
1022
+
1023
+ # --- Assignments ---
1024
+
1025
+ def visit_multi_write_node(node)
1026
+ node.lefts.each_with_index do |target, i|
1027
+ visit(target)
1028
+ emit(", ") if i < node.lefts.size - 1
1029
+ end
1030
+ if node.rest
1031
+ emit(", ") unless node.lefts.empty?
1032
+ visit(node.rest)
1033
+ end
1034
+ emit(" = ")
1035
+ visit(node.value)
1036
+ end
1037
+
1038
+ # --- Require ---
1039
+
1040
+ def visit_call_or_write_node(node)
1041
+ emit(node.location.slice)
1042
+ end
1043
+
1044
+ # --- Super ---
1045
+
1046
+ def visit_super_node(node)
1047
+ emit("super")
1048
+ if node.arguments
1049
+ emit("(")
1050
+ visit(node.arguments)
1051
+ emit(")")
1052
+ elsif node.opening_loc
1053
+ emit("()")
1054
+ end
1055
+ visit(node.block) if node.block
1056
+ end
1057
+
1058
+ def visit_forwarding_super_node(node)
1059
+ emit("super")
1060
+ end
1061
+
1062
+ # --- Defined? ---
1063
+
1064
+ def visit_defined_node(node)
1065
+ emit("defined?(")
1066
+ visit(node.value)
1067
+ emit(")")
1068
+ end
1069
+
1070
+ # --- Alias ---
1071
+
1072
+ def visit_alias_method_node(node)
1073
+ emit("alias ")
1074
+ visit(node.new_name)
1075
+ emit(" ")
1076
+ visit(node.old_name)
1077
+ end
1078
+
1079
+ # --- Parentheses ---
1080
+
1081
+ def visit_parentheses_node(node)
1082
+ emit("(")
1083
+ visit(node.body) if node.body
1084
+ emit(")")
1085
+ end
1086
+
1087
+ # --- Keyword Hash (bare hash in arguments) ---
1088
+
1089
+ def visit_keyword_hash_node(node)
1090
+ node.elements.each_with_index do |elem, i|
1091
+ visit(elem)
1092
+ emit(", ") if i < node.elements.size - 1
1093
+ end
1094
+ end
1095
+
1096
+ # --- Embedded statements in strings ---
1097
+
1098
+ def visit_embedded_statements_node(node)
1099
+ emit(node.location.slice)
1100
+ end
1101
+
1102
+ # --- Numbered/it parameters ---
1103
+
1104
+ def visit_numbered_parameters_node(node)
1105
+ # Implicit, no output
1106
+ end
1107
+
1108
+ def visit_it_parameters_node(node)
1109
+ # Implicit, no output
1110
+ end
1111
+
1112
+ def visit_it_local_variable_read_node(node)
1113
+ emit("it")
1114
+ end
1115
+
1116
+ # --- And/Or ---
1117
+
1118
+ def visit_and_node(node)
1119
+ visit(node.left)
1120
+ # Use && vs 'and' based on source
1121
+ if node.operator_loc.slice == "and"
1122
+ emit(" and ")
1123
+ else
1124
+ emit(" && ")
1125
+ end
1126
+ visit(node.right)
1127
+ end
1128
+
1129
+ def visit_or_node(node)
1130
+ visit(node.left)
1131
+ if node.operator_loc.slice == "or"
1132
+ emit(" or ")
1133
+ else
1134
+ emit(" || ")
1135
+ end
1136
+ visit(node.right)
1137
+ end
1138
+
1139
+ # --- Not ---
1140
+
1141
+ def visit_call_and_write_node(node)
1142
+ emit(node.location.slice)
1143
+ end
1144
+
1145
+ # --- Misc ---
1146
+
1147
+ def visit_frozen_string_literal_comment(node)
1148
+ emit(node.location.slice)
1149
+ end
1150
+
1151
+ def visit_heredoc_node(node)
1152
+ emit(node.location.slice)
1153
+ end
1154
+
1155
+ def visit_match_predicate_node(node)
1156
+ visit(node.value)
1157
+ emit(" in ")
1158
+ visit(node.pattern)
1159
+ end
1160
+
1161
+ def visit_match_required_node(node)
1162
+ visit(node.value)
1163
+ emit(" => ")
1164
+ visit(node.pattern)
1165
+ end
1166
+
1167
+ # Pattern nodes
1168
+ def visit_find_pattern_node(node)
1169
+ emit(node.location.slice)
1170
+ end
1171
+
1172
+ def visit_array_pattern_node(node)
1173
+ emit(node.location.slice)
1174
+ end
1175
+
1176
+ def visit_hash_pattern_node(node)
1177
+ emit(node.location.slice)
1178
+ end
1179
+
1180
+ def visit_pinned_variable_node(node)
1181
+ emit("^")
1182
+ visit(node.variable)
1183
+ end
1184
+
1185
+ def visit_pinned_expression_node(node)
1186
+ emit("^(")
1187
+ visit(node.expression)
1188
+ emit(")")
1189
+ end
1190
+
1191
+ def visit_capture_pattern_node(node)
1192
+ visit(node.value)
1193
+ emit(" => ")
1194
+ visit(node.target)
1195
+ end
1196
+
1197
+ def visit_alternation_pattern_node(node)
1198
+ visit(node.left)
1199
+ emit(" | ")
1200
+ visit(node.right)
1201
+ end
1202
+
1203
+ # --- Ternary (inline if) ---
1204
+
1205
+ def visit_if_node_ternary(node)
1206
+ visit(node.predicate)
1207
+ emit(" ? ")
1208
+ visit(node.statements)
1209
+ emit(" : ")
1210
+ visit(node.else_clause.statements) if node.else_clause
1211
+ end
1212
+ end
1213
+ end
1214
+ end