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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # Indentation-to-INDENT/DEDENT token conversion and error recovery.
6
+ module Indentation
7
+ private
8
+
9
+ def emit_indentation(indent, line_number, line_offset)
10
+ if (indent % 4) != 0
11
+ raise LexError.new("indentation must use multiples of 4 spaces", line: line_number, column: indent + 1, path: @path)
12
+ end
13
+
14
+ current_indent = @indent_stack.last
15
+ if indent == current_indent
16
+ return
17
+ end
18
+
19
+ if indent > current_indent
20
+ if indent != current_indent + 4
21
+ raise LexError.new("indentation may only increase by 4 spaces at a time", line: line_number, column: 1, path: @path)
22
+ end
23
+
24
+ @indent_stack << indent
25
+ @tokens << token(:indent, "", nil, line_number, 1, start_offset: line_offset, end_offset: line_offset)
26
+ return
27
+ end
28
+
29
+ while @indent_stack.last > indent
30
+ @indent_stack.pop
31
+ @tokens << token(:dedent, "", nil, line_number, 1, start_offset: line_offset, end_offset: line_offset)
32
+ end
33
+
34
+ return if @indent_stack.last == indent
35
+
36
+ raise LexError.new("indentation does not match any open block", line: line_number, column: 1, path: @path)
37
+ end
38
+
39
+ def recover_indentation(indent, line_number, line_offset)
40
+ recovered_indent = indent - (indent % 4)
41
+ current_indent = @indent_stack.last
42
+
43
+ if recovered_indent > current_indent + 4
44
+ recovered_indent = current_indent + 4
45
+ end
46
+
47
+ if recovered_indent > current_indent
48
+ @indent_stack << recovered_indent
49
+ @tokens << token(:indent, "", nil, line_number, 1, start_offset: line_offset, end_offset: line_offset)
50
+ return
51
+ end
52
+
53
+ while @indent_stack.last > recovered_indent
54
+ @indent_stack.pop
55
+ @tokens << token(:dedent, "", nil, line_number, 1, start_offset: line_offset, end_offset: line_offset)
56
+ end
57
+
58
+ return if @indent_stack.last == recovered_indent
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # Integer and float literal lexing, including type suffixes and exponents.
6
+ module Numbers
7
+ private
8
+
9
+ def lex_number(line, index, line_number, line_offset:)
10
+ start = index
11
+ type = :integer
12
+
13
+ if line[index] == "0" && %w[x X b B].include?(line[index + 1])
14
+ base_char = line[index + 1]
15
+ index += 2
16
+ allowed = base_char.downcase == "x" ? HEX_DIGIT_BYTE : BIN_DIGIT_BYTE
17
+ while index < line.length && (byte = line.getbyte(index)) && allowed[byte]
18
+ index += 1
19
+ end
20
+ else
21
+ while index < line.length && (byte = line.getbyte(index)) && NUMERIC_PART_BYTE[byte]
22
+ index += 1
23
+ end
24
+
25
+ if line[index] == "." && digit?(line[index + 1])
26
+ type = :float
27
+ index += 1
28
+ while index < line.length && (byte = line.getbyte(index)) && NUMERIC_PART_BYTE[byte]
29
+ index += 1
30
+ end
31
+ end
32
+
33
+ if exponent_part?(line, index)
34
+ type = :float
35
+ index += 1
36
+ index += 1 if %w[+ -].include?(line[index])
37
+ while index < line.length && (byte = line.getbyte(index)) && NUMERIC_PART_BYTE[byte]
38
+ index += 1
39
+ end
40
+ end
41
+ end
42
+
43
+ int_suffix = nil
44
+ if type == :integer
45
+ int_suffix = scan_integer_suffix_at(line, index)
46
+ index += int_suffix.length if int_suffix
47
+ end
48
+
49
+ if type == :float && line[index] == "f" && !identifier_part?((line[index + 1] || " ").to_s)
50
+ index += 1
51
+ elsif type == :float && line[index] == "d" && !identifier_part?((line[index + 1] || " ").to_s)
52
+ index += 1
53
+ end
54
+
55
+ lexeme = line[start...index]
56
+ if type == :integer
57
+ cleaned = int_suffix ? lexeme.delete_suffix(int_suffix).delete("_") : lexeme.delete("_")
58
+ literal = parse_integer(cleaned)
59
+ else
60
+ normalized = lexeme.delete("_").delete_suffix("f").delete_suffix("d")
61
+ literal = normalized.to_f
62
+ end
63
+ @tokens << token(type, lexeme, literal, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index)
64
+ index
65
+ end
66
+
67
+ def scan_integer_suffix_at(line, index)
68
+ return nil if index >= line.length
69
+
70
+ INTEGER_SUFFIX_STRINGS.find do |suffix|
71
+ line[index, suffix.length] == suffix && !identifier_part?((line[index + suffix.length] || " ").to_s)
72
+ end
73
+ end
74
+
75
+ def exponent_part?(line, index)
76
+ return false unless %w[e E].include?(line[index])
77
+
78
+ exponent_index = index + 1
79
+ exponent_index += 1 if %w[+ -].include?(line[exponent_index])
80
+ digit?(line[exponent_index])
81
+ end
82
+
83
+ def parse_integer(lexeme)
84
+ cleaned = lexeme.delete("_")
85
+ case cleaned
86
+ when /\A0[xX]/
87
+ cleaned[2..].to_i(16)
88
+ when /\A0[bB]/
89
+ cleaned[2..].to_i(2)
90
+ else
91
+ cleaned.to_i(10)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # Error-recovery heuristics: detecting a top-level declaration line to
6
+ # resynchronize on after an unterminated grouping or heredoc.
7
+ module Recovery
8
+ private
9
+
10
+ def top_level_resync_line?(line)
11
+ return false if line.strip.empty?
12
+ return false if leading_space_count(line).positive?
13
+
14
+ first_word = line.strip.split(/\s+/, 3)[0]
15
+ second_word = line.strip.split(/\s+/, 3)[1]
16
+
17
+ case first_word
18
+ when *TOP_LEVEL_RESYNC_PREFIXES
19
+ true
20
+ when "async"
21
+ second_word == "function"
22
+ when "public"
23
+ %w[function struct union enum flags variant type const var opaque interface extending attribute event].include?(second_word)
24
+ when "foreign", "external"
25
+ second_word == "function"
26
+ else
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # String literal lexing (including adjacent-literal continuation),
6
+ # character literal lexing, and escape-sequence decoding.
7
+ module Strings
8
+ private
9
+
10
+ def lex_string(lines, line_index, line, index, line_number, line_offset:, cstring: false)
11
+ segment = scan_string_segment(line, index, line_number, cstring:, recover: @recovery_errors)
12
+ consumed_lines = 1
13
+ value = +segment.value
14
+ last_line = line
15
+ last_line_number = line_number
16
+ last_line_offset = line_offset
17
+ last_segment_end = segment.next_index
18
+ last_line_has_newline = lines.fetch(line_index).end_with?("\n")
19
+ remainder = line[segment.next_index..] || ""
20
+ line_indent = leading_space_count(line)
21
+ recovered = segment.recovered
22
+
23
+ if remainder.strip.empty? && !recovered
24
+ scan_line_index = line_index + 1
25
+ scan_line_number = line_number + 1
26
+ scan_line_offset = line_offset + lines.fetch(line_index).bytesize
27
+
28
+ while scan_line_index < lines.length
29
+ raw_line = lines.fetch(scan_line_index)
30
+ scan_line = raw_line.delete_suffix("\n").b
31
+ segment_start = leading_space_count(scan_line)
32
+ prefix = cstring ? 'c"' : '"'
33
+
34
+ break unless segment_start > line_indent
35
+ break if scan_line[segment_start].nil?
36
+ break if scan_line[segment_start] == "#"
37
+ break unless scan_line[segment_start, prefix.length] == prefix
38
+
39
+ continued_segment = scan_string_segment(scan_line, segment_start, scan_line_number, cstring:, recover: @recovery_errors)
40
+ continued_remainder = scan_line[continued_segment.next_index..] || ""
41
+ break unless continued_remainder.strip.empty?
42
+
43
+ value << continued_segment.value
44
+ consumed_lines += 1
45
+ last_line = scan_line
46
+ last_line_number = scan_line_number
47
+ last_line_offset = scan_line_offset
48
+ last_segment_end = continued_segment.next_index
49
+ last_line_has_newline = raw_line.end_with?("\n")
50
+ recovered ||= continued_segment.recovered
51
+
52
+ scan_line_offset += raw_line.bytesize
53
+ scan_line_number += 1
54
+ scan_line_index += 1
55
+
56
+ break if continued_segment.recovered
57
+ end
58
+ end
59
+
60
+ start_offset = line_offset + index
61
+ if consumed_lines == 1
62
+ lexeme = line[index...segment.next_index]
63
+ @tokens << token(cstring ? :cstring : :string, lexeme, value, line_number, index + 1, start_offset:, end_offset: line_offset + segment.next_index)
64
+ return StringLexResult.new(consumed_lines:, next_index: segment.next_index)
65
+ end
66
+
67
+ end_offset = last_line_offset + last_segment_end
68
+ lexeme = @source.byteslice(start_offset, end_offset - start_offset)
69
+ @tokens << token(cstring ? :cstring : :string, lexeme, value, line_number, index + 1, start_offset:, end_offset:)
70
+ emit_line_newline(last_line, last_line_number, last_line_offset, last_line_has_newline)
71
+ StringLexResult.new(consumed_lines:, next_index: last_segment_end)
72
+ end
73
+
74
+ def scan_string_segment(line, index, line_number, cstring: false, recover: false)
75
+ start = index
76
+ index += cstring ? 2 : 1
77
+ value = +""
78
+
79
+ while index < line.length
80
+ char = line[index]
81
+ if char == '"'
82
+ return StringSegment.new(next_index: index + 1, value:)
83
+ end
84
+
85
+ if char == "\\"
86
+ escape = line[index + 1]
87
+ if escape.nil?
88
+ if recover
89
+ @recovery_errors << LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) if @recovery_errors
90
+ return StringSegment.new(next_index: line.length, value:, recovered: true)
91
+ end
92
+
93
+ raise LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path)
94
+ end
95
+
96
+ value << decode_escape(escape)
97
+ index += 2
98
+ next
99
+ end
100
+
101
+ value << char
102
+ index += 1
103
+ end
104
+
105
+ if recover
106
+ @recovery_errors << LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path) if @recovery_errors
107
+ return StringSegment.new(next_index: line.length, value:, recovered: true)
108
+ end
109
+
110
+ raise LexError.new("unterminated string literal", line: line_number, column: start + 1, path: @path)
111
+ end
112
+
113
+ def lex_char_literal(line, index, line_number, line_offset:)
114
+ start = index
115
+ index += 1
116
+ if index >= line.length
117
+ raise LexError.new("unterminated character literal", line: line_number, column: start + 1, path: @path)
118
+ end
119
+
120
+ char = line[index]
121
+ if char == "\\"
122
+ index += 1
123
+ if index >= line.length
124
+ raise LexError.new("unterminated escape in character literal", line: line_number, column: start + 1, path: @path)
125
+ end
126
+ escape_char = line[index]
127
+ if escape_char == "x"
128
+ hex = line[index + 1, 2]
129
+ unless hex&.match?(/\A[0-9a-fA-F]{2}\z/)
130
+ raise LexError.new("invalid hex escape in character literal", line: line_number, column: index + 1, path: @path)
131
+ end
132
+ value = hex.to_i(16)
133
+ index += 3
134
+ else
135
+ value = decode_escape(escape_char).ord
136
+ index += 1
137
+ end
138
+ else
139
+ value = char.ord
140
+ index += 1
141
+ end
142
+
143
+ if index >= line.length || line[index] != "'"
144
+ raise LexError.new("expected closing ' in character literal", line: line_number, column: index + 1, path: @path)
145
+ end
146
+ index += 1
147
+
148
+ lexeme = line[start...index]
149
+ @tokens << token(:char_literal, lexeme, value, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index)
150
+ index
151
+ end
152
+
153
+ def decode_escape(char)
154
+ case char
155
+ when "n" then "\n"
156
+ when "r" then "\r"
157
+ when "t" then "\t"
158
+ when "0" then "\0"
159
+ when '"' then '"'
160
+ when "'" then "'"
161
+ when "\\" then "\\"
162
+ else char
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # Operator / punctuation lexing and grouping-delimiter depth tracking.
6
+ module Symbols
7
+ private
8
+
9
+ def lex_symbol(line, index, line_number, line_offset:)
10
+ start = index
11
+ lexeme = line[index, 3]
12
+ if lexeme && THREE_CHAR_TOKENS.key?(lexeme)
13
+ type = THREE_CHAR_TOKENS.fetch(lexeme)
14
+ @tokens << token(type, lexeme, nil, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index + 3)
15
+ return index + 3
16
+ end
17
+
18
+ lexeme = line[index, 2]
19
+ if lexeme && TWO_CHAR_TOKENS.key?(lexeme)
20
+ type = TWO_CHAR_TOKENS.fetch(lexeme)
21
+ @tokens << token(type, lexeme, nil, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index + 2)
22
+ adjust_grouping_depth(type, line_number, start + 1)
23
+ return index + 2
24
+ end
25
+
26
+ lexeme = line[index]
27
+ type = ONE_CHAR_TOKENS[lexeme]
28
+ unless type
29
+ if @recovery_errors && lexeme == "}"
30
+ @recovery_errors << LexError.new("unexpected closing delimiter", line: line_number, column: start + 1, path: @path)
31
+ return index + 1
32
+ end
33
+
34
+ if @recovery_errors
35
+ @recovery_errors << LexError.new("unexpected character #{lexeme.inspect}", line: line_number, column: start + 1, path: @path)
36
+ return index + 1
37
+ end
38
+
39
+ raise LexError.new("unexpected character #{lexeme.inspect}", line: line_number, column: start + 1, path: @path)
40
+ end
41
+
42
+ @tokens << token(type, lexeme, nil, line_number, start + 1, start_offset: line_offset + start, end_offset: line_offset + index + 1)
43
+ adjust_grouping_depth(type, line_number, start + 1)
44
+ index + 1
45
+ end
46
+
47
+ def adjust_grouping_depth(type, line_number, column)
48
+ case type
49
+ when :lparen, :lbracket
50
+ if @grouping_depth.zero?
51
+ @grouping_start_line = line_number
52
+ @grouping_start_column = column
53
+ end
54
+ @grouping_depth += 1
55
+ when :rparen, :rbracket
56
+ @grouping_depth -= 1
57
+ if @grouping_depth.negative?
58
+ error = LexError.new("unexpected closing delimiter", line: line_number, column: column, path: @path)
59
+ if @recovery_errors
60
+ @recovery_errors << error
61
+ @grouping_depth = 0
62
+ return
63
+ end
64
+
65
+ raise error
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Lexer
5
+ # Trivia collection (comments, blank lines, whitespace) used only when
6
+ # lexing in :with_trivia mode for the concrete syntax tree / formatter.
7
+ module Trivia
8
+ private
9
+
10
+ def with_trivia?
11
+ @mode == :with_trivia
12
+ end
13
+
14
+ def register_detached_line_trivia(kind, line, line_number, line_offset, has_newline:)
15
+ return unless with_trivia?
16
+
17
+ text = has_newline ? (line + "\n") : line
18
+ trivia = TriviaToken.new(
19
+ kind:,
20
+ text:,
21
+ line: line_number,
22
+ column: 1,
23
+ start_offset: line_offset,
24
+ end_offset: line_offset + text.bytesize,
25
+ )
26
+ push_pending_leading_trivia(trivia)
27
+ end
28
+
29
+ def push_pending_leading_trivia(trivia)
30
+ return unless with_trivia?
31
+
32
+ @trivia << trivia
33
+ @pending_leading_trivia << trivia
34
+ end
35
+
36
+ def append_trailing_or_pending(trivia)
37
+ return unless with_trivia?
38
+
39
+ @trivia << trivia
40
+ if @tokens.empty?
41
+ @pending_leading_trivia << trivia
42
+ else
43
+ trailing = @pending_leading_trivia
44
+ @pending_leading_trivia = []
45
+ token = @tokens[-1]
46
+ trailing.each { |entry| token = token.with_appended_trailing_trivia(entry) }
47
+ token = token.with_appended_trailing_trivia(trivia)
48
+ @tokens[-1] = token
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end