mt-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,787 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core"
4
+ require_relative "cst_formatter"
5
+
6
+ module MilkTea
7
+ class FormatterError < StandardError; end
8
+
9
+ class Formatter
10
+ CheckResult = Data.define(:changed, :formatted_source)
11
+ MODES = %i[safe canonical preserve tidy].freeze
12
+ DEFAULT_MAX_LINE_LENGTH = 120
13
+
14
+ def self.format_source(source, path: nil, mode: :safe, max_line_length: nil, profile: nil)
15
+ validate_mode!(mode)
16
+
17
+ case mode
18
+ when :safe, :canonical
19
+ canonical_format(source, path:, profile:)
20
+ when :preserve
21
+ preserve_format(source, path:, profile:)
22
+ when :tidy
23
+ tidy_format(source, path:, max_line_length: resolve_max_line_length(path, explicit: max_line_length), profile:)
24
+ end
25
+ end
26
+
27
+ def self.check_source(source, path: nil, mode: :canonical, max_line_length: nil, profile: nil)
28
+ profile_phase(profile, "format") do
29
+ formatted = format_source(source, path:, mode:, max_line_length:, profile:)
30
+ CheckResult.new(changed: source != formatted, formatted_source: formatted)
31
+ end
32
+ end
33
+
34
+ def self.build_cst(source, path: nil)
35
+ CSTBuilder.build(source, path:)
36
+ end
37
+
38
+ def self.profile_phase(profile, name)
39
+ return yield unless profile
40
+
41
+ profile.measure(name) { yield }
42
+ end
43
+
44
+ def self.canonical_format(source, path:, profile: nil)
45
+ lexed = profile_phase(profile, "format.lex") { Lexer.lex_with_trivia(source, path:) }
46
+ ast = profile_phase(profile, "format.parse") { Parser.parse(source, path:) }
47
+ profile_phase(profile, "format.ast") { PrettyPrinter.format_ast(ast, trivia: lexed.trivia) }
48
+ end
49
+
50
+ def self.preserve_format(source, path:, profile: nil)
51
+ cst = profile_phase(profile, "format.cst") { build_cst(source, path:) }
52
+ profile_phase(profile, "format.cst_fmt") { CSTFormatter.format(cst) }
53
+ end
54
+
55
+ def self.tidy_format(source, path:, max_line_length: DEFAULT_MAX_LINE_LENGTH, profile: nil)
56
+ cst = profile_phase(profile, "format.cst") { build_cst(source, path:) }
57
+ normalized = profile_phase(profile, "format.normalize") { CSTFormatter.format_normalized(cst) }
58
+ wrapped = profile_phase(profile, "format.wrap") { wrap_long_argument_lists(normalized, max_line_length:, path:) }
59
+ profile_phase(profile, "format.blank_lines") { normalize_blank_lines(wrapped, path:) }
60
+ end
61
+
62
+ def self.resolve_max_line_length(path = nil, explicit: nil)
63
+ value = explicit
64
+ if (!value || value.to_i <= 0) && defined?(Linter) && Linter.respond_to?(:load_config)
65
+ value = Linter.load_config(path)&.fetch(:max_line_length, nil)
66
+ end
67
+
68
+ value = value.to_i if value
69
+ value&.positive? ? value : DEFAULT_MAX_LINE_LENGTH
70
+ end
71
+
72
+ def self.wrap_long_argument_lists(source, max_line_length: DEFAULT_MAX_LINE_LENGTH, path: nil)
73
+ return source unless max_line_length.to_i.positive?
74
+
75
+ current = source
76
+ 100.times do
77
+ lines = current.lines
78
+ tokens = Lexer.lex(current, path:)
79
+ tokens_by_line = non_trivia_tokens_by_line(tokens)
80
+ fix = nil
81
+
82
+ lines.each_index do |line_index|
83
+ next unless lines[line_index].delete_suffix("\n").length > max_line_length
84
+
85
+ fix = build_long_line_wrap_fix(current, line_index, max_line_length:, path:, tokens:, tokens_by_line:)
86
+ break if fix
87
+ end
88
+
89
+ break unless fix
90
+
91
+ updated_lines = current.lines
92
+ updated_lines[fix[:start_line_idx]..fix[:end_line_idx]] = [fix[:new_text]]
93
+ updated = updated_lines.join
94
+ break if updated == current
95
+
96
+ current = updated
97
+ end
98
+
99
+ return current if current == source
100
+
101
+ Parser.parse(current, path:)
102
+ current
103
+ rescue StandardError
104
+ source
105
+ end
106
+
107
+ def self.build_long_line_wrap_fix(source, line_index, max_line_length: DEFAULT_MAX_LINE_LENGTH, path: nil, tokens: nil, tokens_by_line: nil, validate: true)
108
+ return nil unless max_line_length.to_i.positive?
109
+
110
+ lines = source.lines
111
+ return nil unless line_index >= 0 && line_index < lines.length
112
+ return nil if line_inside_external_or_foreign_function_header?(lines, line_index)
113
+ return nil if line_inside_fn_type_signature?(lines, line_index)
114
+
115
+ original_line = lines[line_index]
116
+ line = original_line.delete_suffix("\n")
117
+ return nil if line.length <= max_line_length
118
+ return nil unless wrappable_long_line_candidate_text?(line)
119
+
120
+ tokens ||= Lexer.lex(source, path:)
121
+ line_tokens = tokens_by_line ? tokens_by_line.fetch(line_index + 1, []) : non_trivia_tokens_on_line(tokens, line_index + 1)
122
+ candidates = long_line_wrap_candidates(line_tokens, line)
123
+
124
+ indent = line[/\A\s*/] || ""
125
+ arg_indent = indent + " "
126
+ line_terminator = original_line.end_with?("\n") ? "\n" : ""
127
+
128
+ unless candidates.empty?
129
+ candidates
130
+ .sort_by { |entry| [-(entry[:end_char] - entry[:start_char]), entry[:depth]] }
131
+ .each do |candidate|
132
+ new_text = build_wrapped_delimited_group_text(
133
+ line,
134
+ candidate,
135
+ indent:,
136
+ item_indent: arg_indent,
137
+ line_terminator:,
138
+ )
139
+ next if new_text == original_line
140
+
141
+ updated_lines = lines.dup
142
+ updated_lines[line_index..line_index] = [new_text]
143
+ Parser.parse(updated_lines.join, path:) if validate
144
+
145
+ return {
146
+ start_line_idx: line_index,
147
+ end_line_idx: line_index,
148
+ new_text:,
149
+ }
150
+ rescue StandardError
151
+ next if validate
152
+ end
153
+ end
154
+
155
+ logical_chain_fix = build_wrapped_logical_chain_fix(
156
+ lines,
157
+ line_index,
158
+ line,
159
+ line_tokens,
160
+ line_terminator:,
161
+ path:,
162
+ validate:,
163
+ )
164
+ return logical_chain_fix if logical_chain_fix
165
+
166
+ nil
167
+ rescue StandardError
168
+ nil
169
+ end
170
+
171
+ def self.build_wrapped_delimited_group_text(line, candidate, indent:, item_indent:, line_terminator:)
172
+ new_text = +"#{line[0...candidate[:start_char]]}#{candidate[:opening_delimiter]}\n"
173
+ candidate[:arguments].each_with_index do |argument, index|
174
+ suffix = index < candidate[:arguments].length - 1 ? "," : ""
175
+ new_text << "#{item_indent}#{argument}#{suffix}\n"
176
+ end
177
+ new_text << "#{indent}#{candidate[:closing_delimiter]}#{line[candidate[:end_char]..].to_s}#{line_terminator}"
178
+ new_text
179
+ end
180
+
181
+ def self.build_wrapped_logical_chain_fix(lines, line_index, line, line_tokens, line_terminator:, path:, validate: true)
182
+ candidate = logical_chain_wrap_candidate(line_tokens, line)
183
+ return nil unless candidate
184
+
185
+ indent = line[/\A\s*/] || ""
186
+ item_indent = indent + " "
187
+ new_text = build_wrapped_logical_chain_text(
188
+ line,
189
+ candidate,
190
+ indent:,
191
+ item_indent:,
192
+ line_terminator:,
193
+ )
194
+ return nil if new_text == lines[line_index]
195
+
196
+ updated_lines = lines.dup
197
+ updated_lines[line_index..line_index] = [new_text]
198
+ Parser.parse(updated_lines.join, path:) if validate
199
+
200
+ {
201
+ start_line_idx: line_index,
202
+ end_line_idx: line_index,
203
+ new_text:,
204
+ }
205
+ rescue StandardError
206
+ nil
207
+ end
208
+
209
+ def self.build_wrapped_logical_chain_text(line, candidate, indent:, item_indent:, line_terminator:)
210
+ new_text = +"#{line[0...candidate[:start_char]]}(\n"
211
+ new_text << "#{item_indent}#{candidate[:segments].first}\n"
212
+ candidate[:operators].each_with_index do |operator, index|
213
+ new_text << "#{item_indent}#{operator} #{candidate[:segments][index + 1]}\n"
214
+ end
215
+ new_text << "#{indent})#{line[candidate[:end_char]..].to_s}#{line_terminator}"
216
+ new_text
217
+ end
218
+
219
+ # Enforce blank-line rules:
220
+ # - exactly 2 blank lines before top-level function definitions with a body
221
+ # - exactly 2 blank lines before top-level extending blocks
222
+ # - bodyless function declarations: 1 blank line before the first declaration,
223
+ # then 0 blank lines between consecutive declarations
224
+ # - at most 1 blank line everywhere else (constants, variable declarations, expressions)
225
+ # - exactly 1 trailing newline at EOF
226
+ def self.normalize_blank_lines(source, path: nil)
227
+ lines = source.lines(chomp: true)
228
+ result = []
229
+ blank_run = 0
230
+ emitted_content = false
231
+ previous_content_line = nil
232
+ previous_content_index = nil
233
+ previous_top_level_declaration_group = nil
234
+ previous_content_was_bodyful_function = false
235
+ previous_content_was_comment = false
236
+
237
+ lines.each_with_index do |line, line_index|
238
+ if blank_line?(line)
239
+ blank_run += 1
240
+ else
241
+ if emitted_content
242
+ current_top_level_group = top_level_declaration_group(line)
243
+ decorated_function_index = top_level_function_attached_to_attribute_start(lines, line_index)
244
+ needed = if decorated_function_index
245
+ if bodyless_function_header_at?(lines, decorated_function_index)
246
+ 1
247
+ else
248
+ 2
249
+ end
250
+ elsif previous_content_index && line_inside_attribute_application?(lines, previous_content_index)
251
+ 0
252
+ elsif extending_block_header_line?(line)
253
+ if previous_content_was_comment
254
+ 1 # 1 blank line between a comment block and an extending block
255
+ else
256
+ 2 # exactly 2 blank lines before extending blocks
257
+ end
258
+ elsif function_line?(line)
259
+ current_bodyless_function_header = bodyless_function_header_at?(lines, line_index)
260
+ if current_bodyless_function_header
261
+ if previous_content_index && line_inside_bodyless_function_header?(lines, previous_content_index)
262
+ 0 # Keep consecutive declaration-style functions tightly packed.
263
+ elsif previous_content_line && interface_block_header_line?(previous_content_line)
264
+ 0 # First interface method should not have leading blank lines.
265
+ else
266
+ 1 # Separate declaration-style functions from preceding non-function content.
267
+ end
268
+ elsif previous_content_line && extending_block_header_line?(previous_content_line)
269
+ 0 # First method in an extending block should not have leading blank lines.
270
+ elsif previous_content_was_comment
271
+ 1 # 1 blank line between a comment block and a function
272
+ elsif previous_content_was_bodyful_function
273
+ 2 # 2 blank lines between consecutive function definitions
274
+ else
275
+ 2 # 2 blank lines before first function in a scope (after const/struct/enum/import etc.)
276
+ end
277
+ elsif current_top_level_group
278
+ if current_top_level_group == :struct && previous_top_level_declaration_group == :struct
279
+ 1
280
+ elsif current_top_level_group == :enum && previous_top_level_declaration_group == :enum
281
+ 1
282
+ elsif current_top_level_group == :union && previous_top_level_declaration_group == :union
283
+ 1
284
+ elsif previous_top_level_declaration_group && current_top_level_group != previous_top_level_declaration_group
285
+ 1
286
+ else
287
+ [blank_run, 1].min
288
+ end
289
+ else
290
+ [blank_run, 1].min # max 1 blank line elsewhere
291
+ end
292
+ needed.times { result << "" }
293
+ end
294
+ result << line
295
+ blank_run = 0
296
+ emitted_content = true
297
+ previous_content_line = line
298
+ previous_content_index = line_index
299
+ current_top_level_group = top_level_declaration_group(line)
300
+ previous_top_level_declaration_group = current_top_level_group if current_top_level_group
301
+ if decorated_function_index
302
+ unless bodyless_function_header_at?(lines, decorated_function_index)
303
+ previous_content_was_bodyful_function = true
304
+ end
305
+ elsif function_line?(line) && !bodyless_function_header_at?(lines, line_index)
306
+ previous_content_was_bodyful_function = true
307
+ elsif current_top_level_group || extending_block_header_line?(line) || interface_block_header_line?(line) || line_is_import?(line) || line_is_comment?(line)
308
+ previous_content_was_bodyful_function = false
309
+ end
310
+ previous_content_was_comment = line_is_comment?(line)
311
+ end
312
+ end
313
+
314
+ return "" if result.empty?
315
+
316
+ "#{result.join("\n")}\n"
317
+ end
318
+
319
+ def self.function_line?(line)
320
+ bytes = line.bytes
321
+ i = 0
322
+
323
+ i += 1 while i < bytes.length && (bytes[i] == 32 || bytes[i] == 9)
324
+ return false if i >= bytes.length
325
+
326
+ loop do
327
+ word_start = i
328
+ return false unless identifier_head_byte?(bytes[i])
329
+
330
+ i += 1
331
+ i += 1 while i < bytes.length && identifier_tail_byte?(bytes[i])
332
+ word = bytes[word_start...i].pack("C*")
333
+
334
+ if word == "function"
335
+ return i < bytes.length && (bytes[i] == 32 || bytes[i] == 9)
336
+ end
337
+
338
+ return false unless i < bytes.length && (bytes[i] == 32 || bytes[i] == 9)
339
+
340
+ i += 1 while i < bytes.length && (bytes[i] == 32 || bytes[i] == 9)
341
+ return false if i >= bytes.length
342
+ end
343
+ end
344
+
345
+ def self.blank_line?(line)
346
+ line.empty? || line.bytes.all? { |b| b == 32 || b == 9 }
347
+ end
348
+
349
+ def self.identifier_head_byte?(byte)
350
+ (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || byte == 95
351
+ end
352
+
353
+ def self.identifier_tail_byte?(byte)
354
+ identifier_head_byte?(byte) || (byte >= 48 && byte <= 57)
355
+ end
356
+
357
+ def self.bodyless_function_line?(line)
358
+ bytes = line.bytes
359
+ i = bytes.length - 1
360
+ i -= 1 while i >= 0 && (bytes[i] == 32 || bytes[i] == 9)
361
+ return false if i < 0
362
+
363
+ bytes[i] != 58 # ':'
364
+ end
365
+
366
+ def self.bodyless_function_header_at?(lines, line_index)
367
+ line = lines[line_index]
368
+ return false unless function_line?(line)
369
+
370
+ return false unless bodyless_function_line?(line)
371
+
372
+ function_indent = leading_indent_width(line)
373
+ cursor = line_index + 1
374
+
375
+ while cursor < lines.length
376
+ candidate = lines[cursor]
377
+ if blank_line?(candidate)
378
+ cursor += 1
379
+ next
380
+ end
381
+
382
+ candidate_indent = leading_indent_width(candidate)
383
+ if candidate_indent < function_indent
384
+ break
385
+ elsif candidate_indent == function_indent && !function_header_continuation_line?(candidate)
386
+ break
387
+ end
388
+
389
+ return false unless bodyless_function_line?(candidate)
390
+
391
+ cursor += 1
392
+ end
393
+
394
+ true
395
+ end
396
+
397
+ def self.leading_indent_width(line)
398
+ bytes = line.bytes
399
+ i = 0
400
+ i += 1 while i < bytes.length && (bytes[i] == 32 || bytes[i] == 9)
401
+ i
402
+ end
403
+
404
+ def self.function_header_continuation_line?(line)
405
+ stripped = line.strip
406
+ return false if stripped.empty?
407
+
408
+ stripped.start_with?(")", "]", ",", "->")
409
+ end
410
+
411
+ def self.line_inside_bodyless_function_header?(lines, line_index)
412
+ start_index = line_index
413
+ while start_index >= 0
414
+ if function_line?(lines[start_index])
415
+ return bodyless_function_header_at?(lines, start_index) && line_in_function_header_span?(lines, start_index, line_index)
416
+ end
417
+
418
+ start_index -= 1
419
+ end
420
+
421
+ false
422
+ end
423
+
424
+ def self.line_in_function_header_span?(lines, start_index, target_index)
425
+ return false if target_index < start_index
426
+
427
+ function_indent = leading_indent_width(lines[start_index])
428
+ header_end = start_index
429
+ cursor = start_index + 1
430
+
431
+ while cursor < lines.length
432
+ candidate = lines[cursor]
433
+ if blank_line?(candidate)
434
+ cursor += 1
435
+ next
436
+ end
437
+
438
+ candidate_indent = leading_indent_width(candidate)
439
+ if candidate_indent < function_indent
440
+ break
441
+ elsif candidate_indent == function_indent && !function_header_continuation_line?(candidate)
442
+ break
443
+ end
444
+
445
+ header_end = cursor
446
+ cursor += 1
447
+ end
448
+
449
+ target_index <= header_end
450
+ end
451
+
452
+ def self.line_inside_external_or_foreign_function_header?(lines, line_index)
453
+ start_index = line_index
454
+ while start_index >= 0
455
+ if external_or_foreign_function_header_line?(lines[start_index])
456
+ return line_in_function_header_span?(lines, start_index, line_index)
457
+ end
458
+
459
+ break if function_line?(lines[start_index])
460
+ start_index -= 1
461
+ end
462
+
463
+ false
464
+ end
465
+
466
+ def self.external_or_foreign_function_header_line?(line)
467
+ line.strip.match?(/\A(?:[A-Za-z_]\w*\s+)*(?:external|foreign)\s+function\b/)
468
+ end
469
+
470
+ def self.line_inside_fn_type_signature?(lines, line_index)
471
+ start_index = line_index
472
+ while start_index >= 0
473
+ if fn_type_signature_start_line?(lines[start_index])
474
+ return line_in_function_header_span?(lines, start_index, line_index)
475
+ end
476
+
477
+ break if function_line?(lines[start_index]) || external_or_foreign_function_header_line?(lines[start_index])
478
+ start_index -= 1
479
+ end
480
+
481
+ false
482
+ end
483
+
484
+ def self.fn_type_signature_start_line?(line)
485
+ stripped = line.strip
486
+ return true if stripped.match?(/\Atype\s+[A-Za-z_]\w*\s*=\s*fn\s*\(/)
487
+
488
+ stripped.match?(/\A[A-Za-z_]\w*\s*:\s*fn\s*\(/)
489
+ end
490
+
491
+ def self.extending_block_header_line?(line)
492
+ stripped = line.strip
493
+ return false unless stripped.end_with?(":")
494
+
495
+ stripped.start_with?("extending ")
496
+ end
497
+
498
+ def self.interface_block_header_line?(line)
499
+ stripped = line.strip
500
+ return false unless stripped.end_with?(":")
501
+
502
+ stripped.start_with?("interface ")
503
+ end
504
+
505
+ def self.line_is_import?(line)
506
+ line.strip.start_with?("import ")
507
+ end
508
+
509
+ def self.line_is_comment?(line)
510
+ line.strip.start_with?("#")
511
+ end
512
+
513
+ def self.attribute_application_line?(line)
514
+ line.strip.start_with?("@[")
515
+ end
516
+
517
+ def self.attribute_application_end_line(lines, start_index)
518
+ return nil unless start_index >= 0 && start_index < lines.length
519
+ return nil unless attribute_application_line?(lines[start_index])
520
+
521
+ depth = 0
522
+ open_seen = false
523
+ index = start_index
524
+
525
+ while index < lines.length
526
+ bytes = lines[index].bytes
527
+ char_index = 0
528
+ while char_index < bytes.length
529
+ byte = bytes[char_index]
530
+ if byte == 91
531
+ depth += 1
532
+ open_seen = true
533
+ elsif byte == 93 && depth.positive?
534
+ depth -= 1
535
+ return index if open_seen && depth.zero?
536
+ end
537
+
538
+ char_index += 1
539
+ end
540
+
541
+ index += 1
542
+ end
543
+
544
+ nil
545
+ end
546
+
547
+ def self.line_inside_attribute_application?(lines, line_index)
548
+ start_index = line_index
549
+ while start_index >= 0
550
+ if attribute_application_line?(lines[start_index])
551
+ end_index = attribute_application_end_line(lines, start_index)
552
+ return false unless end_index
553
+
554
+ return line_index <= end_index
555
+ end
556
+
557
+ break if function_line?(lines[start_index])
558
+ start_index -= 1
559
+ end
560
+
561
+ false
562
+ end
563
+
564
+ def self.top_level_function_attached_to_attribute_start(lines, line_index)
565
+ return nil unless leading_indent_width(lines[line_index]).zero?
566
+ return nil unless attribute_application_line?(lines[line_index])
567
+ return nil if line_index.positive? && line_inside_attribute_application?(lines, line_index - 1)
568
+
569
+ cursor = line_index
570
+ loop do
571
+ end_index = attribute_application_end_line(lines, cursor)
572
+ return nil unless end_index
573
+
574
+ cursor = end_index + 1
575
+ cursor += 1 while cursor < lines.length && blank_line?(lines[cursor])
576
+ return nil if cursor >= lines.length
577
+
578
+ if attribute_application_line?(lines[cursor]) && leading_indent_width(lines[cursor]).zero?
579
+ next
580
+ end
581
+
582
+ return cursor if function_line?(lines[cursor]) && leading_indent_width(lines[cursor]).zero?
583
+
584
+ return nil
585
+ end
586
+ end
587
+
588
+ def self.top_level_declaration_group(line)
589
+ stripped = line.strip
590
+ return nil if stripped.empty?
591
+ return nil unless leading_indent_width(line).zero?
592
+
593
+ first_token = stripped[/\A[A-Za-z_][A-Za-z0-9_]*/]
594
+ return nil unless first_token
595
+
596
+ case first_token
597
+ when "link", "include", "compiler_flag"
598
+ :directive
599
+ when "opaque"
600
+ :opaque
601
+ when "type"
602
+ :type
603
+ when "struct"
604
+ :struct
605
+ when "union"
606
+ :union
607
+ when "variant"
608
+ :variant
609
+ when "enum"
610
+ :enum
611
+ when "flags"
612
+ :flags
613
+ when "const"
614
+ :const
615
+ when "var"
616
+ :var
617
+ when "external"
618
+ stripped.match?(/\Aexternal\s+function\b/) ? :external_function : :external
619
+ else
620
+ nil
621
+ end
622
+ end
623
+
624
+ def self.long_line_wrap_candidates(line_tokens, line)
625
+ stack = []
626
+ candidates = []
627
+
628
+ line_tokens.each_with_index do |token, index|
629
+ case token.type
630
+ when :lparen, :lbracket
631
+ stack << { token:, index:, depth: stack.length }
632
+ when :rparen, :rbracket
633
+ opening = stack.pop
634
+ next unless opening
635
+ next unless matching_delimiter_pair?(opening[:token].type, token.type)
636
+
637
+ candidate = build_long_line_wrap_candidate(
638
+ line,
639
+ line_tokens,
640
+ opening[:index],
641
+ index,
642
+ opening[:depth],
643
+ )
644
+ candidates << candidate if candidate
645
+ end
646
+ end
647
+
648
+ candidates
649
+ end
650
+
651
+ def self.logical_chain_wrap_candidate(line_tokens, line)
652
+ return nil if line_tokens.length < 4
653
+
654
+ header_length = case line_tokens.first.type
655
+ when :if, :while
656
+ 1
657
+ when :else
658
+ line_tokens[1]&.type == :if ? 2 : 0
659
+ else
660
+ 0
661
+ end
662
+ return nil if header_length.zero?
663
+
664
+ colon_token = line_tokens.reverse.find { |token| token.type == :colon }
665
+ return nil unless colon_token
666
+
667
+ expression_start_token = line_tokens[header_length]
668
+ return nil unless expression_start_token
669
+
670
+ start_char = expression_start_token.column - 1
671
+ return nil if line[start_char] == "("
672
+
673
+ nested_group_depth = 0
674
+ operators = []
675
+ segments = []
676
+ current_start = start_char
677
+
678
+ line_tokens[header_length...line_tokens.index(colon_token)].each do |token|
679
+ case token.type
680
+ when :lparen, :lbracket
681
+ nested_group_depth += 1
682
+ when :rparen, :rbracket
683
+ nested_group_depth -= 1 if nested_group_depth.positive?
684
+ when :and, :or
685
+ next unless nested_group_depth.zero?
686
+
687
+ segment = line[current_start...(token.column - 1)].to_s.strip
688
+ return nil if segment.empty?
689
+
690
+ segments << segment
691
+ operators << token.lexeme
692
+ current_start = token.column - 1 + token.lexeme.length
693
+ end
694
+ end
695
+
696
+ return nil if operators.empty?
697
+
698
+ final_segment = line[current_start...(colon_token.column - 1)].to_s.strip
699
+ return nil if final_segment.empty?
700
+
701
+ segments << final_segment
702
+ {
703
+ start_char:,
704
+ end_char: colon_token.column - 1,
705
+ operators:,
706
+ segments:,
707
+ }
708
+ end
709
+
710
+ def self.matching_delimiter_pair?(opening_type, closing_type)
711
+ (opening_type == :lparen && closing_type == :rparen) ||
712
+ (opening_type == :lbracket && closing_type == :rbracket)
713
+ end
714
+
715
+ def self.build_long_line_wrap_candidate(line, line_tokens, open_index, close_index, depth)
716
+ open_token = line_tokens[open_index]
717
+ close_token = line_tokens[close_index]
718
+ nested_group_depth = 0
719
+ comma_tokens = []
720
+
721
+ (open_index + 1...close_index).each do |index|
722
+ token = line_tokens[index]
723
+ case token.type
724
+ when :lparen, :lbracket
725
+ nested_group_depth += 1
726
+ when :rparen, :rbracket
727
+ nested_group_depth -= 1 if nested_group_depth.positive?
728
+ when :comma
729
+ comma_tokens << token if nested_group_depth.zero? && token.line == open_token.line
730
+ end
731
+ end
732
+
733
+ return nil if comma_tokens.empty?
734
+
735
+ start_char = open_token.column - 1
736
+ end_char = close_token.column
737
+ current_start = start_char + 1
738
+ arguments = []
739
+
740
+ comma_tokens.each do |comma|
741
+ argument = line[current_start...(comma.column - 1)].to_s.strip
742
+ return nil if argument.empty?
743
+
744
+ arguments << argument
745
+ current_start = comma.column
746
+ end
747
+
748
+ final_argument = line[current_start...(close_token.column - 1)].to_s.strip
749
+ return nil if final_argument.empty?
750
+
751
+ arguments << final_argument
752
+ {
753
+ depth:,
754
+ start_char:,
755
+ end_char:,
756
+ opening_delimiter: open_token.lexeme,
757
+ closing_delimiter: close_token.lexeme,
758
+ arguments:,
759
+ }
760
+ end
761
+
762
+ def self.wrappable_long_line_candidate_text?(line)
763
+ return true if line.include?("(") || line.include?("[")
764
+ return true if line.include?(" and ") || line.include?(" or ")
765
+
766
+ false
767
+ end
768
+
769
+ def self.non_trivia_tokens_on_line(tokens, line_number)
770
+ tokens.select { |token| token.line == line_number && !%i[newline indent dedent eof].include?(token.type) }
771
+ end
772
+
773
+ def self.non_trivia_tokens_by_line(tokens)
774
+ tokens.each_with_object(Hash.new { |hash, line| hash[line] = [] }) do |token, by_line|
775
+ next if %i[newline indent dedent eof].include?(token.type)
776
+
777
+ by_line[token.line] << token
778
+ end
779
+ end
780
+
781
+ def self.validate_mode!(mode)
782
+ return if MODES.include?(mode)
783
+
784
+ raise FormatterError, "unknown formatter mode #{mode.inspect}"
785
+ end
786
+ end
787
+ end