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,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "transport"
4
+ require_relative "document_manager"
5
+
6
+ module Konpeito
7
+ module LSP
8
+ # Main LSP server that handles client requests
9
+ class Server
10
+ attr_reader :transport, :document_manager
11
+
12
+ def initialize(input: $stdin, output: $stdout)
13
+ @transport = Transport.new(input: input, output: output)
14
+ @document_manager = DocumentManager.new(@transport)
15
+ @running = false
16
+ @initialized = false
17
+ end
18
+
19
+ # Start the LSP server main loop
20
+ def start
21
+ @running = true
22
+ while @running
23
+ request = @transport.read
24
+ break unless request
25
+
26
+ response = handle_request(request)
27
+ @transport.write(response) if response
28
+ end
29
+ end
30
+
31
+ # Stop the server
32
+ def stop
33
+ @running = false
34
+ end
35
+
36
+ # Handle a single request (exposed for testing)
37
+ # @param request [Hash] The LSP request
38
+ # @return [Hash, nil] The response or nil for notifications
39
+ def handle_request(request)
40
+ method = request[:method]
41
+ id = request[:id]
42
+ params = request[:params] || {}
43
+
44
+ result = case method
45
+ when "initialize"
46
+ handle_initialize(params)
47
+ when "initialized"
48
+ # Notification, no response needed
49
+ nil
50
+ when "shutdown"
51
+ handle_shutdown
52
+ when "exit"
53
+ @running = false
54
+ nil
55
+ when "textDocument/didOpen"
56
+ handle_did_open(params)
57
+ when "textDocument/didChange"
58
+ handle_did_change(params)
59
+ when "textDocument/didClose"
60
+ handle_did_close(params)
61
+ when "textDocument/hover"
62
+ handle_hover(params)
63
+ when "textDocument/completion"
64
+ handle_completion(params)
65
+ when "textDocument/definition"
66
+ handle_definition(params)
67
+ when "textDocument/references"
68
+ handle_references(params)
69
+ when "textDocument/rename"
70
+ handle_rename(params)
71
+ when "textDocument/prepareRename"
72
+ handle_prepare_rename(params)
73
+ else
74
+ # Unknown method
75
+ if id
76
+ { error: { code: -32601, message: "Method not found: #{method}" } }
77
+ end
78
+ end
79
+
80
+ # Build response for requests (not notifications)
81
+ if id && result != :notification
82
+ { jsonrpc: "2.0", id: id, result: result }
83
+ else
84
+ nil
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def handle_initialize(params)
91
+ @initialized = true
92
+
93
+ {
94
+ capabilities: {
95
+ textDocumentSync: {
96
+ openClose: true,
97
+ change: 1 # Full content sync
98
+ },
99
+ hoverProvider: true,
100
+ completionProvider: {
101
+ triggerCharacters: ["."],
102
+ resolveProvider: false
103
+ },
104
+ definitionProvider: true,
105
+ referencesProvider: true,
106
+ renameProvider: {
107
+ prepareProvider: true
108
+ }
109
+ },
110
+ serverInfo: {
111
+ name: "konpeito-lsp",
112
+ version: Konpeito::VERSION
113
+ }
114
+ }
115
+ end
116
+
117
+ def handle_shutdown
118
+ @initialized = false
119
+ nil
120
+ end
121
+
122
+ def handle_did_open(params)
123
+ uri = params.dig(:textDocument, :uri)
124
+ text = params.dig(:textDocument, :text)
125
+ @document_manager.open(uri, text)
126
+ :notification
127
+ end
128
+
129
+ def handle_did_change(params)
130
+ uri = params.dig(:textDocument, :uri)
131
+ # We use full sync, so first contentChanges has full text
132
+ changes = params[:contentChanges]
133
+ text = changes&.first&.dig(:text)
134
+ @document_manager.change(uri, text) if text
135
+ :notification
136
+ end
137
+
138
+ def handle_did_close(params)
139
+ uri = params.dig(:textDocument, :uri)
140
+ @document_manager.close(uri)
141
+ :notification
142
+ end
143
+
144
+ def handle_hover(params)
145
+ uri = params.dig(:textDocument, :uri)
146
+ position = params[:position]
147
+ @document_manager.hover(uri, position)
148
+ end
149
+
150
+ def handle_completion(params)
151
+ uri = params.dig(:textDocument, :uri)
152
+ position = params[:position]
153
+ @document_manager.completion(uri, position)
154
+ end
155
+
156
+ def handle_definition(params)
157
+ uri = params.dig(:textDocument, :uri)
158
+ position = params[:position]
159
+ @document_manager.definition(uri, position)
160
+ end
161
+
162
+ def handle_references(params)
163
+ uri = params.dig(:textDocument, :uri)
164
+ position = params[:position]
165
+ include_declaration = params.dig(:context, :includeDeclaration) != false
166
+ @document_manager.references(uri, position, include_declaration: include_declaration)
167
+ end
168
+
169
+ def handle_rename(params)
170
+ uri = params.dig(:textDocument, :uri)
171
+ position = params[:position]
172
+ new_name = params[:newName]
173
+ @document_manager.rename(uri, position, new_name)
174
+ end
175
+
176
+ def handle_prepare_rename(params)
177
+ uri = params.dig(:textDocument, :uri)
178
+ position = params[:position]
179
+ @document_manager.prepare_rename(uri, position)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "language_server-protocol"
4
+
5
+ module Konpeito
6
+ module LSP
7
+ # Handles LSP JSON-RPC communication over stdio
8
+ class Transport
9
+ def initialize(input: $stdin, output: $stdout)
10
+ @reader = LanguageServer::Protocol::Transport::Io::Reader.new(input)
11
+ @writer = LanguageServer::Protocol::Transport::Io::Writer.new(output)
12
+ end
13
+
14
+ # Read next request/notification from client
15
+ # @return [Hash, nil] The request object or nil if EOF
16
+ def read
17
+ @reader.read
18
+ end
19
+
20
+ # Write response to client
21
+ # @param response [Hash] The response object
22
+ def write(response)
23
+ @writer.write(response)
24
+ end
25
+
26
+ # Send notification to client (no response expected)
27
+ # @param method [String] The notification method
28
+ # @param params [Hash] The notification parameters
29
+ def notify(method, params)
30
+ @writer.write({
31
+ jsonrpc: "2.0",
32
+ method: method,
33
+ params: params
34
+ })
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Konpeito
6
+ module Parser
7
+ class PrismAdapter
8
+ class << self
9
+ def parse(source, filepath: "(eval)")
10
+ result = Prism.parse(source, filepath: filepath)
11
+
12
+ unless result.success?
13
+ errors = result.errors.map { |e| format_error(e, filepath) }
14
+ raise ParseError, "Parse errors:\n#{errors.join("\n")}"
15
+ end
16
+
17
+ result.value
18
+ end
19
+
20
+ def parse_file(filepath)
21
+ source = File.read(filepath)
22
+ parse(source, filepath: filepath)
23
+ end
24
+
25
+ # Detect require and require_relative calls in AST
26
+ # Returns array of { type: :require | :require_relative, name: String }
27
+ def detect_requires(ast)
28
+ requires = []
29
+ visitor = RequireVisitor.new(requires)
30
+ visitor.visit(ast)
31
+ requires
32
+ end
33
+
34
+ private
35
+
36
+ def format_error(error, filepath)
37
+ loc = error.location
38
+ " #{filepath}:#{loc.start_line}:#{loc.start_column}: #{error.message}"
39
+ end
40
+ end
41
+
42
+ # Visitor to detect require/require_relative calls
43
+ class RequireVisitor < Prism::Visitor
44
+ def initialize(requires)
45
+ @requires = requires
46
+ end
47
+
48
+ def visit_call_node(node)
49
+ if node.receiver.nil? && %w[require require_relative].include?(node.name.to_s)
50
+ arg = node.arguments&.arguments&.first
51
+ if arg.is_a?(Prism::StringNode)
52
+ @requires << {
53
+ type: node.name.to_sym,
54
+ name: arg.unescaped,
55
+ line: node.location.start_line,
56
+ column: node.location.start_column
57
+ }
58
+ end
59
+ end
60
+ super
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Konpeito
6
+ module Platform
7
+ def self.windows?
8
+ !!(RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/)
9
+ end
10
+
11
+ def self.macos?
12
+ !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
13
+ end
14
+
15
+ def self.linux?
16
+ !!(RbConfig::CONFIG["host_os"] =~ /linux/)
17
+ end
18
+
19
+ # CRuby extension file extension
20
+ def self.shared_lib_extension
21
+ if macos? then ".bundle"
22
+ elsif windows? then ".dll"
23
+ else ".so"
24
+ end
25
+ end
26
+
27
+ # JVM classpath separator
28
+ def self.classpath_separator
29
+ windows? ? ";" : ":"
30
+ end
31
+
32
+ # Platform-independent executable finder (replaces `which`)
33
+ def self.find_executable(name)
34
+ exts = windows? ? (ENV["PATHEXT"] || ".COM;.EXE;.BAT").split(";") : [""]
35
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |dir|
36
+ exts.each do |ext|
37
+ path = File.join(dir, "#{name}#{ext}")
38
+ return path if File.executable?(path) && !File.directory?(path)
39
+ end
40
+ end
41
+ nil
42
+ end
43
+
44
+ # Find LLVM tool (clang, llc, opt) with version suffix fallback
45
+ def self.find_llvm_tool(name)
46
+ ["#{name}-20", "#{name}-19", "#{name}-18", name].each do |tool|
47
+ path = find_executable(tool)
48
+ return path if path
49
+ end
50
+ platform_llvm_paths(name).each { |p| return p if File.exist?(p) }
51
+ nil
52
+ end
53
+
54
+ # Find LLVM shared library for FFI (debug_info.rb)
55
+ def self.find_llvm_lib
56
+ candidates = if macos?
57
+ ["/opt/homebrew/opt/llvm@20/lib/libLLVM-20.dylib",
58
+ "/usr/local/opt/llvm@20/lib/libLLVM-20.dylib"]
59
+ elsif windows?
60
+ ["C:/Program Files/LLVM/bin/LLVM-C.dll"]
61
+ else
62
+ ["/usr/lib/llvm-20/lib/libLLVM-20.so", "/usr/lib/llvm-20/lib/libLLVM.so",
63
+ "/usr/lib64/llvm20/lib/libLLVM-20.so", "/usr/lib/libLLVM-20.so"]
64
+ end
65
+ candidates.find { |p| File.exist?(p) }
66
+ end
67
+
68
+ def self.default_java_home
69
+ if macos? then "/opt/homebrew/opt/openjdk@21"
70
+ elsif windows? then "C:/Program Files/Java/jdk-21"
71
+ else "/usr/lib/jvm/java-21-openjdk"
72
+ end
73
+ end
74
+
75
+ def self.java_install_hint
76
+ if macos? then "brew install openjdk@21"
77
+ elsif windows? then "winget install EclipseAdoptium.Temurin.21.JDK"
78
+ else "sudo apt install openjdk-21-jdk (Ubuntu) / sudo dnf install java-21-openjdk-devel (Fedora)"
79
+ end
80
+ end
81
+
82
+ def self.llvm_install_hint
83
+ if macos? then "brew install llvm@20"
84
+ elsif windows? then "winget install LLVM.LLVM"
85
+ else "sudo apt install llvm-20 clang-20 (Ubuntu) / sudo dnf install llvm20 clang20 (Fedora)"
86
+ end
87
+ end
88
+
89
+ def self.debugger_tune
90
+ macos? ? "lldb" : "gdb"
91
+ end
92
+
93
+ private_class_method def self.platform_llvm_paths(name)
94
+ if macos?
95
+ ["/opt/homebrew/opt/llvm@20/bin/#{name}", "/usr/local/opt/llvm@20/bin/#{name}"]
96
+ elsif windows?
97
+ ["C:/Program Files/LLVM/bin/#{name}.exe"]
98
+ else
99
+ ["/usr/lib/llvm-20/bin/#{name}", "/usr/lib/llvm-19/bin/#{name}", "/usr/bin/#{name}"]
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Konpeito
6
+ module Profile
7
+ # Represents profiling data for a single function
8
+ class FunctionProfile
9
+ attr_reader :name, :calls, :time_ms, :percent
10
+
11
+ def initialize(name:, calls:, time_ms:, percent:)
12
+ @name = name
13
+ @calls = calls
14
+ @time_ms = time_ms
15
+ @percent = percent
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ name: @name,
21
+ calls: @calls,
22
+ time_ms: @time_ms,
23
+ percent: @percent
24
+ }
25
+ end
26
+ end
27
+
28
+ # Reads and formats profile reports from JSON files
29
+ class Report
30
+ attr_reader :functions, :total_time_ms
31
+
32
+ def initialize(json_path)
33
+ raise ArgumentError, "Profile file not found: #{json_path}" unless File.exist?(json_path)
34
+
35
+ @json_path = json_path
36
+ data = JSON.parse(File.read(json_path))
37
+ @functions = data["functions"].map do |f|
38
+ FunctionProfile.new(
39
+ name: f["name"],
40
+ calls: f["calls"],
41
+ time_ms: f["time_ms"],
42
+ percent: f["percent"]
43
+ )
44
+ end.sort_by { |f| -f.time_ms }
45
+ @total_time_ms = data["total_time_ms"]
46
+ end
47
+
48
+ def to_text(max_name_length: 40)
49
+ lines = []
50
+ lines << "Konpeito Profile Report"
51
+ lines << "=" * 76
52
+ lines << ""
53
+ lines << format("%-#{max_name_length}s %12s %12s %8s", "Function", "Calls", "Time (ms)", "%")
54
+ lines << format("%-#{max_name_length}s %12s %12s %8s",
55
+ "-" * max_name_length, "-" * 12, "-" * 12, "-" * 8)
56
+
57
+ @functions.each do |f|
58
+ name = truncate_name(f.name, max_name_length)
59
+ lines << format("%-#{max_name_length}s %12d %12.3f %7.2f%%",
60
+ name, f.calls, f.time_ms, f.percent)
61
+ end
62
+
63
+ lines << ""
64
+ lines << "Total time: #{@total_time_ms.round(3)} ms"
65
+ lines.join("\n")
66
+ end
67
+
68
+ def to_json_pretty
69
+ JSON.pretty_generate({
70
+ functions: @functions.map(&:to_h),
71
+ total_time_ms: @total_time_ms
72
+ })
73
+ end
74
+
75
+ def hottest_functions(n = 5)
76
+ @functions.first(n)
77
+ end
78
+
79
+ def most_called_functions(n = 5)
80
+ @functions.sort_by { |f| -f.calls }.first(n)
81
+ end
82
+
83
+ # Get the path to the flame graph folded file
84
+ def flame_graph_path
85
+ @json_path.sub(/\.json$/, ".folded")
86
+ end
87
+
88
+ # Check if flame graph data exists
89
+ def flame_graph_available?
90
+ File.exist?(flame_graph_path)
91
+ end
92
+
93
+ # Read flame graph data as array of [stack, samples]
94
+ def flame_graph_stacks
95
+ return [] unless flame_graph_available?
96
+
97
+ File.readlines(flame_graph_path).map do |line|
98
+ parts = line.strip.split(" ")
99
+ samples = parts.pop.to_i
100
+ stack = parts.join(" ")
101
+ [stack, samples]
102
+ end.sort_by { |_, samples| -samples }
103
+ end
104
+
105
+ # Generate flame graph text summary
106
+ def flame_graph_summary(max_stacks: 10)
107
+ stacks = flame_graph_stacks.first(max_stacks)
108
+ return "No flame graph data available" if stacks.empty?
109
+
110
+ lines = []
111
+ lines << "Flame Graph Stack Summary (Top #{max_stacks})"
112
+ lines << "=" * 76
113
+ lines << ""
114
+
115
+ total_samples = flame_graph_stacks.sum { |_, s| s }
116
+ stacks.each do |stack, samples|
117
+ percent = total_samples > 0 ? (samples * 100.0 / total_samples) : 0
118
+ lines << format("%7d (%5.1f%%) %s", samples, percent, stack)
119
+ end
120
+
121
+ lines << ""
122
+ lines << "To generate flame graph SVG:"
123
+ lines << " flamegraph.pl #{flame_graph_path} > profile.svg"
124
+ lines.join("\n")
125
+ end
126
+
127
+ private
128
+
129
+ def truncate_name(name, max_length)
130
+ return name if name.length <= max_length
131
+
132
+ "#{name[0, max_length - 3]}..."
133
+ end
134
+ end
135
+ end
136
+ end