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,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ # Known standard library names that can be loaded at runtime via rb_require
5
+ # This list is derived from Ruby's bundled RBS stdlib definitions
6
+ KNOWN_STDLIB_LIBRARIES = %w[
7
+ json fileutils find pathname tempfile timeout uri yaml
8
+ digest openssl socket stringio csv date time set
9
+ net/http net/https net/ftp net/smtp net/pop net/imap
10
+ securerandom base64 benchmark erb logger optparse
11
+ pp prettyprint pstore monitor mutex_m thwait
12
+ tsort weakref shellwords abbrev ostruct open-uri
13
+ singleton forwardable delegate observable ripper
14
+ drb cgi webrick etc
15
+ ].freeze
16
+
17
+ # Wrapper for merged AST that provides ProgramNode-like interface
18
+ class MergedAST
19
+ attr_reader :statements, :locals
20
+
21
+ def initialize(base_ast, merged_statements)
22
+ @locals = base_ast.locals
23
+ @statements = MergedStatements.new(merged_statements)
24
+ end
25
+
26
+ def is_a?(klass)
27
+ return true if klass == Prism::ProgramNode
28
+ super
29
+ end
30
+
31
+ def compact_child_nodes
32
+ [@statements]
33
+ end
34
+
35
+ # TypedNode.node_type derives from class name — mimic Prism::ProgramNode
36
+ def class
37
+ Prism::ProgramNode
38
+ end
39
+ end
40
+
41
+ class MergedStatements
42
+ attr_reader :body
43
+
44
+ def initialize(body)
45
+ @body = body
46
+ end
47
+
48
+ def compact_child_nodes
49
+ @body
50
+ end
51
+
52
+ def is_a?(klass)
53
+ return true if klass == Prism::StatementsNode
54
+ super
55
+ end
56
+
57
+ # TypedNode.node_type derives from class name — mimic Prism::StatementsNode
58
+ def class
59
+ Prism::StatementsNode
60
+ end
61
+ end
62
+
63
+ class DependencyResolver
64
+ attr_reader :resolved_files, :rbs_paths, :stdlib_requires, :runtime_native_extensions
65
+
66
+ def initialize(base_paths: [], verbose: false, cache_manager: nil)
67
+ @base_paths = base_paths
68
+ @verbose = verbose
69
+ @cache_manager = cache_manager
70
+ @resolved_files = {} # path => AST
71
+ @resolving = Set.new # For circular dependency detection
72
+ @rbs_paths = [] # Auto-detected RBS paths
73
+ @resolve_order = [] # Order of resolved files (for AST merging)
74
+ @stdlib_requires = [] # Stdlib libraries to load at runtime
75
+ @runtime_native_extensions = [] # Native extensions loaded at runtime (not for linking)
76
+ @cache_hits = 0 # Statistics for verbose output
77
+ @cache_misses = 0
78
+ end
79
+
80
+ # Resolve all dependencies starting from entry_file
81
+ # Returns [merged_ast, rbs_paths, stdlib_requires, runtime_native_extensions]
82
+ def resolve(entry_file)
83
+ entry_path = File.expand_path(entry_file)
84
+ resolve_file(entry_path)
85
+
86
+ # Log cache statistics in verbose mode
87
+ if @verbose && @cache_manager && (@cache_hits > 0 || @cache_misses > 0)
88
+ log "Cache: #{@cache_hits} hits, #{@cache_misses} misses"
89
+ end
90
+
91
+ merged_ast = merge_asts
92
+ [merged_ast, @rbs_paths.uniq, @stdlib_requires.uniq, @runtime_native_extensions.uniq]
93
+ end
94
+
95
+ private
96
+
97
+ def resolve_file(path)
98
+ return if @resolved_files.key?(path)
99
+
100
+ if @resolving.include?(path)
101
+ cycle = @resolving.to_a
102
+ cycle_start = cycle.index(path)
103
+ cycle_files = cycle[cycle_start..].map { |p| File.basename(p) }
104
+ cycle_files << File.basename(path) # Complete the cycle
105
+ cycle_path = cycle_files.join(" -> ")
106
+ raise DependencyError.new(
107
+ "Circular dependency detected: #{cycle_path}",
108
+ from_file: @resolving.to_a.last,
109
+ cycle: cycle_files
110
+ )
111
+ end
112
+
113
+ unless File.exist?(path)
114
+ raise DependencyError.new(
115
+ "File not found: #{path}",
116
+ missing_file: path
117
+ )
118
+ end
119
+
120
+ log "Resolving: #{path}"
121
+ @resolving.add(path)
122
+
123
+ # Try to use cached AST if available
124
+ ast = nil
125
+ if @cache_manager
126
+ cached_ast = @cache_manager.get_ast(path)
127
+ if cached_ast
128
+ log " (cached)"
129
+ ast = cached_ast
130
+ @cache_hits += 1
131
+ end
132
+ end
133
+
134
+ # Parse the file if not cached
135
+ unless ast
136
+ ast = Parser::PrismAdapter.parse_file(path)
137
+ @cache_misses += 1
138
+
139
+ # Store in cache
140
+ if @cache_manager
141
+ @cache_manager.put_ast(path, ast)
142
+ end
143
+ end
144
+
145
+ @resolved_files[path] = ast
146
+
147
+ # Check for corresponding RBS file (alongside .rb, or in types/ subdirectory)
148
+ rbs_path = path.sub(/\.rb$/, ".rbs")
149
+ if File.exist?(rbs_path)
150
+ log " Found RBS: #{rbs_path}"
151
+ @rbs_paths << rbs_path
152
+ else
153
+ dir = File.dirname(path)
154
+ # Check types/ subdirectory (common convention for separate type definitions)
155
+ types_rbs = File.join(dir, "types", File.basename(path, ".rb") + ".rbs")
156
+ if File.exist?(types_rbs)
157
+ log " Found RBS: #{types_rbs}"
158
+ @rbs_paths << types_rbs
159
+ else
160
+ # Check parent dir's types/ with subdirectory name as filename
161
+ # e.g., widgets/text.rb -> ../types/widgets.rbs
162
+ parent_dir = File.dirname(dir)
163
+ subdir_name = File.basename(dir)
164
+ parent_types_rbs = File.join(parent_dir, "types", subdir_name + ".rbs")
165
+ if File.exist?(parent_types_rbs) && !@rbs_paths.include?(parent_types_rbs)
166
+ log " Found RBS: #{parent_types_rbs}"
167
+ @rbs_paths << parent_types_rbs
168
+ end
169
+ end
170
+ end
171
+
172
+ # Clear existing dependencies for this file (in case of re-analysis)
173
+ if @cache_manager
174
+ @cache_manager.clear_dependencies(path)
175
+ end
176
+
177
+ # Detect and resolve requires
178
+ requires = Parser::PrismAdapter.detect_requires(ast)
179
+ requires.each do |req|
180
+ dep_path = find_file(req[:name], from_file: path, is_relative: req[:type] == :require_relative)
181
+ if dep_path
182
+ # Register dependency: this file depends on dep_path
183
+ if @cache_manager
184
+ @cache_manager.add_dependency(path, dep_path)
185
+ end
186
+ resolve_file(dep_path)
187
+ elsif req[:type] == :require && stdlib_library?(req[:name])
188
+ # Known stdlib library - will be loaded at runtime via rb_require
189
+ log " Detected stdlib require: #{req[:name]}"
190
+ @stdlib_requires << req[:name]
191
+ elsif req[:type] == :require_relative
192
+ # Check if this points to a native extension (.bundle/.so/.dll)
193
+ if native_extension_exists?(req[:name], from_file: path)
194
+ log " Detected native extension (runtime load): #{req[:name]}"
195
+ # Track both the base name (for linker exclusion) and the absolute path
196
+ # (for rb_require in Init function).
197
+ # e.g. "../stdlib/ui/konpeito_ui" → base: "konpeito_ui", path: "/abs/path/to/konpeito_ui"
198
+ abs_path = File.expand_path(req[:name], File.dirname(path))
199
+ @runtime_native_extensions << { base: File.basename(req[:name]), path: abs_path }
200
+ else
201
+ # require_relative must be resolvable
202
+ raise DependencyError.new(
203
+ "Cannot resolve require_relative: #{req[:name]} from #{path}",
204
+ from_file: path,
205
+ line: req[:line],
206
+ missing_file: req[:name]
207
+ )
208
+ end
209
+ else
210
+ # Unknown require - likely a gem that must be required at runtime
211
+ raise DependencyError.new(
212
+ "Cannot resolve require: #{req[:name]}. " \
213
+ "If this is a gem, use require at runtime in Ruby code instead of compile-time.",
214
+ from_file: path,
215
+ line: req[:line],
216
+ missing_file: req[:name]
217
+ )
218
+ end
219
+ end
220
+
221
+ @resolving.delete(path)
222
+ @resolve_order << path
223
+ end
224
+
225
+ def find_file(name, from_file:, is_relative:)
226
+ # Add .rb extension if not present
227
+ name_with_ext = name.end_with?(".rb") ? name : "#{name}.rb"
228
+
229
+ if is_relative
230
+ # require_relative: resolve relative to the current file
231
+ dir = File.dirname(from_file)
232
+ path = File.expand_path(name_with_ext, dir)
233
+ return path if File.exist?(path)
234
+ else
235
+ # require: search in base_paths and relative to entry file
236
+ search_paths = @base_paths + [File.dirname(from_file)]
237
+ search_paths.each do |base|
238
+ path = File.expand_path(name_with_ext, base)
239
+ return path if File.exist?(path)
240
+ end
241
+ end
242
+
243
+ nil # Not found (may be stdlib)
244
+ end
245
+
246
+ def merge_asts
247
+ return @resolved_files.values.first if @resolve_order.size == 1
248
+
249
+ # Merge all ASTs in dependency order (dependencies first)
250
+ all_statements = []
251
+
252
+ @resolve_order.each do |path|
253
+ ast = @resolved_files[path]
254
+ next unless ast.is_a?(Prism::ProgramNode)
255
+
256
+ # Extract statements, filtering out require/require_relative calls
257
+ ast.statements.body.each do |stmt|
258
+ next if require_statement?(stmt)
259
+ all_statements << stmt
260
+ end
261
+ end
262
+
263
+ # Create a wrapper that provides the same interface as ProgramNode
264
+ first_ast = @resolved_files[@resolve_order.first]
265
+ MergedAST.new(first_ast, all_statements)
266
+ end
267
+
268
+ def require_statement?(stmt)
269
+ return false unless stmt.is_a?(Prism::CallNode)
270
+ return false unless stmt.receiver.nil?
271
+ %w[require require_relative].include?(stmt.name.to_s)
272
+ end
273
+
274
+ def log(message)
275
+ puts message if @verbose
276
+ end
277
+
278
+ # Check if a require_relative target points to a native extension (.bundle/.so/.dll)
279
+ def native_extension_exists?(name, from_file:)
280
+ dir = File.dirname(from_file)
281
+ base = File.expand_path(name, dir)
282
+ # Check for common native extension file extensions
283
+ %w[.bundle .so .dll .dylib].any? { |ext| File.exist?("#{base}#{ext}") }
284
+ end
285
+
286
+ # Check if a name is a known stdlib library
287
+ def stdlib_library?(name)
288
+ # Normalize: convert dashes to underscores for matching
289
+ normalized = name.tr("-", "_")
290
+ KNOWN_STDLIB_LIBRARIES.include?(name) ||
291
+ KNOWN_STDLIB_LIBRARIES.include?(normalized) ||
292
+ KNOWN_STDLIB_LIBRARIES.include?(name.tr("_", "-"))
293
+ end
294
+ end
295
+
296
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "diagnostic"
4
+ require_relative "renderer"
5
+
6
+ module Konpeito
7
+ module Diagnostics
8
+ # Collects diagnostics from various compilation phases
9
+ class Collector
10
+ attr_reader :diagnostics
11
+
12
+ def initialize
13
+ @diagnostics = []
14
+ @source_cache = {} # file_path -> source content
15
+ end
16
+
17
+ # Register source content for a file
18
+ def register_source(file_path, content)
19
+ @source_cache[file_path] = content
20
+ end
21
+
22
+ # Get cached source content
23
+ def source_for(file_path)
24
+ @source_cache[file_path] ||= File.exist?(file_path) ? File.read(file_path) : nil
25
+ end
26
+
27
+ # Add a diagnostic
28
+ def add(diagnostic)
29
+ @diagnostics << diagnostic
30
+ end
31
+
32
+ # Create and add a type mismatch error
33
+ def type_mismatch(expected:, found:, node:, file_path:)
34
+ span = span_from_node(node, file_path)
35
+ add(Diagnostic.type_mismatch(expected: expected, found: found, span: span))
36
+ end
37
+
38
+ # Create and add an undefined variable error
39
+ def undefined_variable(name:, node:, file_path:, similar: nil)
40
+ span = span_from_node(node, file_path)
41
+ add(Diagnostic.undefined_variable(name: name, span: span, similar: similar))
42
+ end
43
+
44
+ # Create and add a parse error
45
+ def parse_error(message:, location:, file_path:)
46
+ span = span_from_location(location, file_path)
47
+ add(Diagnostic.parse_error(message: message, span: span))
48
+ end
49
+
50
+ # Create and add a circular dependency error
51
+ def circular_dependency(cycle:, file_path:, line: 1)
52
+ span = SourceSpan.new(
53
+ file_path: file_path,
54
+ start_line: line,
55
+ start_column: 0,
56
+ source: source_for(file_path)
57
+ )
58
+ add(Diagnostic.circular_dependency(cycle: cycle, span: span))
59
+ end
60
+
61
+ # Create and add a file not found error
62
+ def file_not_found(path:, from_file:, line: 1)
63
+ span = SourceSpan.new(
64
+ file_path: from_file,
65
+ start_line: line,
66
+ start_column: 0,
67
+ source: source_for(from_file)
68
+ )
69
+ add(Diagnostic.file_not_found(path: path, span: span))
70
+ end
71
+
72
+ # Create and add a codegen error
73
+ def codegen_error(message:, node: nil, file_path: nil)
74
+ span = node && file_path ? span_from_node(node, file_path) : nil
75
+ add(Diagnostic.codegen_error(message: message, span: span))
76
+ end
77
+
78
+ # Create and add an arity mismatch error
79
+ def arity_mismatch(expected:, found:, node:, file_path:)
80
+ span = span_from_node(node, file_path)
81
+ add(Diagnostic.arity_mismatch(expected: expected, found: found, span: span))
82
+ end
83
+
84
+ # Check if there are any errors
85
+ def errors?
86
+ @diagnostics.any?(&:error?)
87
+ end
88
+
89
+ # Get only errors
90
+ def errors
91
+ @diagnostics.select(&:error?)
92
+ end
93
+
94
+ # Get only warnings
95
+ def warnings
96
+ @diagnostics.select(&:warning?)
97
+ end
98
+
99
+ # Render all diagnostics
100
+ def render(color: true, io: $stderr)
101
+ renderer = DiagnosticRenderer.new(color: color, io: io)
102
+ renderer.render_all(@diagnostics)
103
+ end
104
+
105
+ # Clear all diagnostics
106
+ def clear
107
+ @diagnostics.clear
108
+ end
109
+
110
+ private
111
+
112
+ def span_from_node(node, file_path)
113
+ return nil unless node.respond_to?(:location) && node.location
114
+
115
+ span_from_location(node.location, file_path)
116
+ end
117
+
118
+ def span_from_location(location, file_path)
119
+ SourceSpan.from_prism_location(
120
+ location,
121
+ file_path: file_path,
122
+ source: source_for(file_path)
123
+ )
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ module Diagnostics
5
+ # Represents a location in source code with ability to extract snippets
6
+ class SourceSpan
7
+ attr_reader :file_path, :start_line, :start_column, :end_line, :end_column
8
+
9
+ def initialize(file_path:, start_line:, start_column:, end_line: nil, end_column: nil, source: nil)
10
+ @file_path = file_path
11
+ @start_line = start_line
12
+ @start_column = start_column
13
+ @end_line = end_line || start_line
14
+ @end_column = end_column || start_column
15
+ @source = source
16
+ end
17
+
18
+ # Create from a Prism node location
19
+ def self.from_prism_location(location, file_path:, source: nil)
20
+ new(
21
+ file_path: file_path,
22
+ start_line: location.start_line,
23
+ start_column: location.start_column,
24
+ end_line: location.end_line,
25
+ end_column: location.end_column,
26
+ source: source
27
+ )
28
+ end
29
+
30
+ # Get source lines with optional context
31
+ def snippet(context_lines: 2)
32
+ return nil unless source_lines
33
+
34
+ first_line = [@start_line - context_lines, 1].max
35
+ last_line = [@end_line + context_lines, source_lines.size].min
36
+
37
+ lines = []
38
+ (first_line..last_line).each do |line_num|
39
+ line_content = source_lines[line_num - 1] || ""
40
+ lines << { line_num: line_num, content: line_content, highlight: line_in_span?(line_num) }
41
+ end
42
+ lines
43
+ end
44
+
45
+ # Get only the highlighted source text
46
+ def highlighted_text
47
+ return nil unless source_lines
48
+
49
+ if @start_line == @end_line
50
+ line = source_lines[@start_line - 1] || ""
51
+ line[@start_column...@end_column] || line[@start_column..]
52
+ else
53
+ # Multi-line span
54
+ lines = source_lines[(@start_line - 1)...@end_line]
55
+ return nil unless lines
56
+
57
+ result = []
58
+ lines.each_with_index do |line, idx|
59
+ if idx == 0
60
+ result << line[@start_column..]
61
+ elsif idx == lines.size - 1
62
+ result << line[0...@end_column]
63
+ else
64
+ result << line
65
+ end
66
+ end
67
+ result.join("\n")
68
+ end
69
+ end
70
+
71
+ def to_s
72
+ "#{@file_path}:#{@start_line}:#{@start_column}"
73
+ end
74
+
75
+ private
76
+
77
+ def source_lines
78
+ return @source_lines if defined?(@source_lines)
79
+
80
+ @source_lines = if @source
81
+ @source.lines
82
+ elsif @file_path && File.exist?(@file_path)
83
+ File.read(@file_path).lines
84
+ end
85
+ end
86
+
87
+ def line_in_span?(line_num)
88
+ line_num >= @start_line && line_num <= @end_line
89
+ end
90
+ end
91
+
92
+ # Represents a diagnostic label (additional annotation on source)
93
+ class Label
94
+ attr_reader :span, :message, :style
95
+
96
+ def initialize(span:, message:, style: :primary)
97
+ @span = span
98
+ @message = message
99
+ @style = style # :primary, :secondary
100
+ end
101
+ end
102
+
103
+ # Represents a single diagnostic message (error, warning, or note)
104
+ class Diagnostic
105
+ SEVERITIES = { error: 0, warning: 1, note: 2, help: 3 }.freeze
106
+
107
+ attr_reader :severity, :code, :message, :span, :labels, :notes, :suggestions
108
+
109
+ def initialize(severity:, code:, message:, span: nil, labels: [], notes: [], suggestions: [])
110
+ raise ArgumentError, "Invalid severity: #{severity}" unless SEVERITIES.key?(severity)
111
+
112
+ @severity = severity
113
+ @code = code
114
+ @message = message
115
+ @span = span
116
+ @labels = labels
117
+ @notes = notes
118
+ @suggestions = suggestions
119
+ end
120
+
121
+ def error?
122
+ @severity == :error
123
+ end
124
+
125
+ def warning?
126
+ @severity == :warning
127
+ end
128
+
129
+ # Create a type mismatch error
130
+ def self.type_mismatch(expected:, found:, span:, note_span: nil)
131
+ labels = [Label.new(span: span, message: "expected #{expected}, found #{found}")]
132
+
133
+ notes = []
134
+ if note_span
135
+ notes << "type was inferred here: #{note_span}"
136
+ end
137
+
138
+ new(
139
+ severity: :error,
140
+ code: "E001",
141
+ message: "type mismatch",
142
+ span: span,
143
+ labels: labels,
144
+ notes: notes
145
+ )
146
+ end
147
+
148
+ # Create an undefined variable error
149
+ def self.undefined_variable(name:, span:, similar: nil)
150
+ suggestions = []
151
+ if similar
152
+ suggestions << "did you mean `#{similar}`?"
153
+ end
154
+
155
+ new(
156
+ severity: :error,
157
+ code: "E004",
158
+ message: "undefined variable `#{name}`",
159
+ span: span,
160
+ suggestions: suggestions
161
+ )
162
+ end
163
+
164
+ # Create an undefined method error
165
+ def self.undefined_method(name:, receiver_type:, span:)
166
+ new(
167
+ severity: :error,
168
+ code: "E005",
169
+ message: "undefined method `#{name}` for type #{receiver_type}",
170
+ span: span
171
+ )
172
+ end
173
+
174
+ # Create a parse error
175
+ def self.parse_error(message:, span:)
176
+ new(
177
+ severity: :error,
178
+ code: "E010",
179
+ message: message,
180
+ span: span
181
+ )
182
+ end
183
+
184
+ # Create a circular dependency error
185
+ def self.circular_dependency(cycle:, span:)
186
+ cycle_str = cycle.join(" -> ")
187
+ new(
188
+ severity: :error,
189
+ code: "E020",
190
+ message: "circular dependency detected",
191
+ span: span,
192
+ notes: ["dependency cycle: #{cycle_str}"]
193
+ )
194
+ end
195
+
196
+ # Create a file not found error
197
+ def self.file_not_found(path:, span:)
198
+ new(
199
+ severity: :error,
200
+ code: "E021",
201
+ message: "file not found: #{path}",
202
+ span: span
203
+ )
204
+ end
205
+
206
+ # Create a codegen error
207
+ def self.codegen_error(message:, span: nil)
208
+ new(
209
+ severity: :error,
210
+ code: "E030",
211
+ message: message,
212
+ span: span
213
+ )
214
+ end
215
+
216
+ # Create an arity mismatch error
217
+ def self.arity_mismatch(expected:, found:, span:)
218
+ new(
219
+ severity: :error,
220
+ code: "E002",
221
+ message: "wrong number of arguments (given #{found}, expected #{expected})",
222
+ span: span
223
+ )
224
+ end
225
+
226
+ # Create an occurs check error (infinite type)
227
+ def self.occurs_check(type_var:, type:, span:)
228
+ new(
229
+ severity: :error,
230
+ code: "E003",
231
+ message: "cannot construct infinite type: #{type_var} ~ #{type}",
232
+ span: span
233
+ )
234
+ end
235
+ end
236
+ end
237
+ end