type-guessr 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +138 -0
- data/lib/ruby_lsp/type_guessr/config.rb +90 -0
- data/lib/ruby_lsp/type_guessr/debug_server.rb +861 -0
- data/lib/ruby_lsp/type_guessr/graph_builder.rb +349 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +565 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +506 -0
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +200 -0
- data/lib/type-guessr.rb +28 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +1649 -0
- data/lib/type_guessr/core/converter/rbs_converter.rb +88 -0
- data/lib/type_guessr/core/index/location_index.rb +72 -0
- data/lib/type_guessr/core/inference/resolver.rb +664 -0
- data/lib/type_guessr/core/inference/result.rb +41 -0
- data/lib/type_guessr/core/ir/nodes.rb +599 -0
- data/lib/type_guessr/core/logger.rb +43 -0
- data/lib/type_guessr/core/rbs_provider.rb +304 -0
- data/lib/type_guessr/core/registry/method_registry.rb +106 -0
- data/lib/type_guessr/core/registry/variable_registry.rb +87 -0
- data/lib/type_guessr/core/signature_provider.rb +101 -0
- data/lib/type_guessr/core/type_simplifier.rb +64 -0
- data/lib/type_guessr/core/types.rb +425 -0
- data/lib/type_guessr/version.rb +5 -0
- metadata +81 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
require "cgi"
|
|
6
|
+
require_relative "graph_builder"
|
|
7
|
+
|
|
8
|
+
module RubyLsp
|
|
9
|
+
module TypeGuessr
|
|
10
|
+
# Debug web server for inspecting TypeGuessr index data
|
|
11
|
+
# Only runs when debug mode is enabled
|
|
12
|
+
# Provides search and IR graph visualization
|
|
13
|
+
class DebugServer
|
|
14
|
+
DEFAULT_PORT = 7010
|
|
15
|
+
|
|
16
|
+
def initialize(global_state, runtime_adapter, port: DEFAULT_PORT)
|
|
17
|
+
@global_state = global_state
|
|
18
|
+
@runtime_adapter = runtime_adapter
|
|
19
|
+
@graph_builder = GraphBuilder.new(runtime_adapter)
|
|
20
|
+
@port = port
|
|
21
|
+
@server = nil
|
|
22
|
+
@thread = nil
|
|
23
|
+
@running = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
return if @running
|
|
28
|
+
|
|
29
|
+
@running = true
|
|
30
|
+
@thread = Thread.new { run_server }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop
|
|
34
|
+
@running = false
|
|
35
|
+
@server&.close
|
|
36
|
+
@thread&.kill
|
|
37
|
+
@thread = nil
|
|
38
|
+
@server = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def run_server
|
|
44
|
+
@server = TCPServer.new("127.0.0.1", @port)
|
|
45
|
+
warn("[TypeGuessr DebugServer] Listening on 127.0.0.1:#{@port}")
|
|
46
|
+
|
|
47
|
+
while @running
|
|
48
|
+
begin
|
|
49
|
+
client = @server.accept
|
|
50
|
+
handle_request(client)
|
|
51
|
+
rescue IOError, Errno::EBADF
|
|
52
|
+
# Server closed, exit gracefully
|
|
53
|
+
break
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
warn("[TypeGuessr DebugServer] Request error: #{e.class}: #{e.message}")
|
|
56
|
+
warn("[TypeGuessr DebugServer] #{e.backtrace&.first(3)&.join("\n")}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
rescue Errno::EADDRINUSE
|
|
60
|
+
warn("[TypeGuessr DebugServer] Port #{@port} is already in use")
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
warn("[TypeGuessr DebugServer] Server error: #{e.class}: #{e.message}")
|
|
63
|
+
warn("[TypeGuessr DebugServer] #{e.backtrace&.first(5)&.join("\n")}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_request(client)
|
|
67
|
+
request_line = client.gets
|
|
68
|
+
return client.close if request_line.nil?
|
|
69
|
+
|
|
70
|
+
method, full_path, = request_line.split
|
|
71
|
+
return client.close if method != "GET"
|
|
72
|
+
|
|
73
|
+
# Read headers (discard them)
|
|
74
|
+
while (line = client.gets) && line != "\r\n"
|
|
75
|
+
# skip headers
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse path and query string
|
|
79
|
+
path, query_string = full_path.split("?", 2)
|
|
80
|
+
params = parse_query_string(query_string)
|
|
81
|
+
|
|
82
|
+
response = route_request(path, params)
|
|
83
|
+
send_response(client, response)
|
|
84
|
+
ensure
|
|
85
|
+
client.close
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_query_string(query_string)
|
|
89
|
+
return {} unless query_string
|
|
90
|
+
|
|
91
|
+
query_string.split("&").to_h do |pair|
|
|
92
|
+
key, value = pair.split("=", 2)
|
|
93
|
+
[key, CGI.unescape(value || "")]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def route_request(path, params)
|
|
98
|
+
case path
|
|
99
|
+
when "/"
|
|
100
|
+
index_page
|
|
101
|
+
when "/api/search"
|
|
102
|
+
search_api(params["q"] || "")
|
|
103
|
+
when "/api/graph"
|
|
104
|
+
graph_api(params["node_key"] || "")
|
|
105
|
+
when "/api/keys"
|
|
106
|
+
keys_api(params["q"] || "")
|
|
107
|
+
when "/graph"
|
|
108
|
+
graph_page(params["node_key"] || "")
|
|
109
|
+
else
|
|
110
|
+
not_found
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def send_response(client, response)
|
|
115
|
+
client.print "HTTP/1.1 #{response[:status]}\r\n"
|
|
116
|
+
client.print "Content-Type: #{response[:content_type]}\r\n"
|
|
117
|
+
client.print "Content-Length: #{response[:body].bytesize}\r\n"
|
|
118
|
+
client.print "Connection: close\r\n"
|
|
119
|
+
client.print "\r\n"
|
|
120
|
+
client.print response[:body]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# API Endpoints
|
|
124
|
+
|
|
125
|
+
def search_api(query)
|
|
126
|
+
return json_response({ query: query, results: [] }) if query.empty?
|
|
127
|
+
|
|
128
|
+
results = @runtime_adapter.search_project_methods(query)
|
|
129
|
+
json_response({ query: query, results: results })
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def keys_api(query)
|
|
133
|
+
all_keys = @runtime_adapter.instance_variable_get(:@location_index)
|
|
134
|
+
.instance_variable_get(:@key_index).keys
|
|
135
|
+
keys = if query.empty?
|
|
136
|
+
all_keys.first(100)
|
|
137
|
+
else
|
|
138
|
+
all_keys.select { |k| k.include?(query) }.first(100)
|
|
139
|
+
end
|
|
140
|
+
json_response({ query: query, total: all_keys.size, keys: keys })
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def graph_api(node_key)
|
|
144
|
+
return json_error("node_key parameter required", 400) if node_key.empty?
|
|
145
|
+
|
|
146
|
+
begin
|
|
147
|
+
warn("[TypeGuessr DebugServer] graph_api called with: #{node_key}")
|
|
148
|
+
graph_data = @graph_builder.build(node_key)
|
|
149
|
+
warn("[TypeGuessr DebugServer] graph_data built: #{graph_data ? "success" : "nil"}")
|
|
150
|
+
|
|
151
|
+
unless graph_data
|
|
152
|
+
# Debug: show available keys that start with similar prefix
|
|
153
|
+
all_keys = @runtime_adapter.instance_variable_get(:@location_index)
|
|
154
|
+
.instance_variable_get(:@key_index).keys
|
|
155
|
+
similar = all_keys.select { |k| k.include?(node_key.split(":").first) }.first(10)
|
|
156
|
+
return json_error("Node not found: #{node_key}. Similar keys: #{similar}", 404)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
json_response(graph_data)
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
warn("[TypeGuessr DebugServer] graph_api error: #{e.class}: #{e.message}")
|
|
162
|
+
warn("[TypeGuessr DebugServer] #{e.backtrace&.first(5)&.join("\n")}")
|
|
163
|
+
json_error("Internal error: #{e.message}", 500)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def json_response(data)
|
|
168
|
+
{
|
|
169
|
+
status: "200 OK",
|
|
170
|
+
content_type: "application/json; charset=utf-8",
|
|
171
|
+
body: JSON.generate(data)
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def json_error(message, status_code)
|
|
176
|
+
status_text = status_code == 404 ? "Not Found" : "Bad Request"
|
|
177
|
+
{
|
|
178
|
+
status: "#{status_code} #{status_text}",
|
|
179
|
+
content_type: "application/json; charset=utf-8",
|
|
180
|
+
body: JSON.generate({ error: message })
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def not_found
|
|
185
|
+
{
|
|
186
|
+
status: "404 Not Found",
|
|
187
|
+
content_type: "text/html; charset=utf-8",
|
|
188
|
+
body: "<h1>404 Not Found</h1>"
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# HTML Pages
|
|
193
|
+
|
|
194
|
+
def index_page
|
|
195
|
+
{
|
|
196
|
+
status: "200 OK",
|
|
197
|
+
content_type: "text/html; charset=utf-8",
|
|
198
|
+
body: <<~HTML
|
|
199
|
+
<!DOCTYPE html>
|
|
200
|
+
<html lang="en">
|
|
201
|
+
<head>
|
|
202
|
+
<meta charset="UTF-8">
|
|
203
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
204
|
+
<title>TypeGuessr Debug Console</title>
|
|
205
|
+
<style>
|
|
206
|
+
* { box-sizing: border-box; }
|
|
207
|
+
body {
|
|
208
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
209
|
+
margin: 0;
|
|
210
|
+
padding: 20px;
|
|
211
|
+
background: #1e1e1e;
|
|
212
|
+
color: #d4d4d4;
|
|
213
|
+
}
|
|
214
|
+
.container { max-width: 900px; margin: 0 auto; }
|
|
215
|
+
h1 { color: #569cd6; margin-bottom: 10px; }
|
|
216
|
+
.subtitle { color: #808080; margin-bottom: 30px; }
|
|
217
|
+
.search-box { margin-bottom: 20px; }
|
|
218
|
+
.search-box input {
|
|
219
|
+
width: 100%;
|
|
220
|
+
padding: 12px 16px;
|
|
221
|
+
font-size: 16px;
|
|
222
|
+
background: #2d2d2d;
|
|
223
|
+
border: 1px solid #3e3e3e;
|
|
224
|
+
color: #d4d4d4;
|
|
225
|
+
border-radius: 6px;
|
|
226
|
+
outline: none;
|
|
227
|
+
}
|
|
228
|
+
.search-box input:focus { border-color: #569cd6; }
|
|
229
|
+
.search-box input::placeholder { color: #808080; }
|
|
230
|
+
.results { margin-top: 20px; }
|
|
231
|
+
.result-item {
|
|
232
|
+
padding: 14px 16px;
|
|
233
|
+
margin: 8px 0;
|
|
234
|
+
background: #2d2d2d;
|
|
235
|
+
border-left: 3px solid #569cd6;
|
|
236
|
+
border-radius: 0 6px 6px 0;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: background 0.15s, transform 0.1s;
|
|
239
|
+
display: flex;
|
|
240
|
+
justify-content: space-between;
|
|
241
|
+
align-items: center;
|
|
242
|
+
}
|
|
243
|
+
.result-item:hover { background: #3e3e3e; transform: translateX(4px); }
|
|
244
|
+
.class-name { color: #4ec9b0; font-weight: 600; }
|
|
245
|
+
.method-name { color: #dcdcaa; }
|
|
246
|
+
.location { color: #808080; font-size: 0.9em; }
|
|
247
|
+
.empty-state {
|
|
248
|
+
text-align: center;
|
|
249
|
+
padding: 40px;
|
|
250
|
+
color: #808080;
|
|
251
|
+
}
|
|
252
|
+
.stats {
|
|
253
|
+
margin-top: 30px;
|
|
254
|
+
padding: 15px;
|
|
255
|
+
background: #2d2d2d;
|
|
256
|
+
border-radius: 6px;
|
|
257
|
+
font-size: 0.9em;
|
|
258
|
+
color: #808080;
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
261
|
+
</head>
|
|
262
|
+
<body>
|
|
263
|
+
<div class="container">
|
|
264
|
+
<h1>TypeGuessr Debug Console</h1>
|
|
265
|
+
<p class="subtitle">Search for classes and methods to visualize their IR dependency graphs</p>
|
|
266
|
+
|
|
267
|
+
<div class="search-box">
|
|
268
|
+
<input type="text" id="search" placeholder="Search methods (e.g., User#save, Recipe, save)" autofocus>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div id="results" class="results">
|
|
272
|
+
<div class="empty-state">Type to search for methods...</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="stats" id="stats">Loading stats...</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<script>
|
|
279
|
+
const searchInput = document.getElementById('search');
|
|
280
|
+
const resultsDiv = document.getElementById('results');
|
|
281
|
+
const statsDiv = document.getElementById('stats');
|
|
282
|
+
let debounceTimer;
|
|
283
|
+
|
|
284
|
+
// Load stats
|
|
285
|
+
const stats = #{JSON.generate(@runtime_adapter.stats)};
|
|
286
|
+
statsDiv.textContent = `Indexed: ${stats.files_count} files, ${stats.total_nodes} nodes`;
|
|
287
|
+
|
|
288
|
+
searchInput.addEventListener('input', (e) => {
|
|
289
|
+
clearTimeout(debounceTimer);
|
|
290
|
+
debounceTimer = setTimeout(() => search(e.target.value), 300);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
async function search(query) {
|
|
294
|
+
if (query.length < 1) {
|
|
295
|
+
resultsDiv.innerHTML = '<div class="empty-state">Type to search for methods...</div>';
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
|
301
|
+
const data = await response.json();
|
|
302
|
+
displayResults(data.results);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
resultsDiv.innerHTML = '<div class="empty-state">Error searching...</div>';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function displayResults(results) {
|
|
309
|
+
if (results.length === 0) {
|
|
310
|
+
resultsDiv.innerHTML = '<div class="empty-state">No results found</div>';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
resultsDiv.innerHTML = results.map(r => `
|
|
315
|
+
<div class="result-item" onclick="viewGraph('${encodeURIComponent(r.node_key)}')">
|
|
316
|
+
<div>
|
|
317
|
+
<span class="class-name">${escapeHtml(r.class_name)}</span>#<span class="method-name">${escapeHtml(r.method_name)}</span>
|
|
318
|
+
</div>
|
|
319
|
+
<span class="location">Line ${r.location.line || '?'}</span>
|
|
320
|
+
</div>
|
|
321
|
+
`).join('');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function escapeHtml(text) {
|
|
325
|
+
const div = document.createElement('div');
|
|
326
|
+
div.textContent = text;
|
|
327
|
+
return div.innerHTML;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function viewGraph(nodeKey) {
|
|
331
|
+
window.location.href = `/graph?node_key=${nodeKey}`;
|
|
332
|
+
}
|
|
333
|
+
</script>
|
|
334
|
+
</body>
|
|
335
|
+
</html>
|
|
336
|
+
HTML
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def graph_page(node_key)
|
|
341
|
+
return not_found if node_key.empty?
|
|
342
|
+
|
|
343
|
+
{
|
|
344
|
+
status: "200 OK",
|
|
345
|
+
content_type: "text/html; charset=utf-8",
|
|
346
|
+
body: <<~HTML
|
|
347
|
+
<!DOCTYPE html>
|
|
348
|
+
<html lang="en">
|
|
349
|
+
<head>
|
|
350
|
+
<meta charset="UTF-8">
|
|
351
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
352
|
+
<title>IR Graph - TypeGuessr</title>
|
|
353
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
354
|
+
<style>
|
|
355
|
+
* { box-sizing: border-box; }
|
|
356
|
+
body {
|
|
357
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
358
|
+
margin: 0;
|
|
359
|
+
padding: 20px;
|
|
360
|
+
background: #1e1e1e;
|
|
361
|
+
color: #d4d4d4;
|
|
362
|
+
}
|
|
363
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
364
|
+
h1 { color: #569cd6; margin: 10px 0; font-size: 1.5em; }
|
|
365
|
+
.back-link {
|
|
366
|
+
color: #569cd6;
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
display: inline-block;
|
|
369
|
+
margin-bottom: 10px;
|
|
370
|
+
}
|
|
371
|
+
.back-link:hover { text-decoration: underline; }
|
|
372
|
+
.graph-container {
|
|
373
|
+
position: relative;
|
|
374
|
+
}
|
|
375
|
+
.zoom-controls {
|
|
376
|
+
position: absolute;
|
|
377
|
+
top: 10px;
|
|
378
|
+
right: 10px;
|
|
379
|
+
z-index: 10;
|
|
380
|
+
display: flex;
|
|
381
|
+
gap: 4px;
|
|
382
|
+
}
|
|
383
|
+
.zoom-btn {
|
|
384
|
+
width: 32px;
|
|
385
|
+
height: 32px;
|
|
386
|
+
border: 1px solid #ccc;
|
|
387
|
+
background: #fff;
|
|
388
|
+
border-radius: 4px;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
font-size: 18px;
|
|
391
|
+
font-weight: bold;
|
|
392
|
+
color: #333;
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
justify-content: center;
|
|
396
|
+
}
|
|
397
|
+
.zoom-btn:hover { background: #f0f0f0; }
|
|
398
|
+
.zoom-btn:active { background: #e0e0e0; }
|
|
399
|
+
#graph {
|
|
400
|
+
background: #ffffff;
|
|
401
|
+
padding: 20px;
|
|
402
|
+
border-radius: 6px;
|
|
403
|
+
overflow: auto;
|
|
404
|
+
min-height: 600px;
|
|
405
|
+
cursor: grab;
|
|
406
|
+
}
|
|
407
|
+
#graph.dragging {
|
|
408
|
+
cursor: grabbing;
|
|
409
|
+
user-select: none;
|
|
410
|
+
}
|
|
411
|
+
#graph svg {
|
|
412
|
+
transform-origin: top left;
|
|
413
|
+
transition: transform 0.1s ease;
|
|
414
|
+
}
|
|
415
|
+
.node-details {
|
|
416
|
+
margin-top: 20px;
|
|
417
|
+
padding: 16px;
|
|
418
|
+
background: #2d2d2d;
|
|
419
|
+
border-radius: 6px;
|
|
420
|
+
display: none;
|
|
421
|
+
}
|
|
422
|
+
.node-details.visible { display: block; }
|
|
423
|
+
.def-inspect {
|
|
424
|
+
margin-top: 20px;
|
|
425
|
+
padding: 16px;
|
|
426
|
+
background: #2d2d2d;
|
|
427
|
+
border-radius: 6px;
|
|
428
|
+
}
|
|
429
|
+
.def-inspect h3 { color: #4ec9b0; margin: 0 0 12px 0; font-size: 1.1em; }
|
|
430
|
+
.def-inspect pre {
|
|
431
|
+
background: #1e1e1e;
|
|
432
|
+
padding: 12px;
|
|
433
|
+
border-radius: 4px;
|
|
434
|
+
overflow-x: auto;
|
|
435
|
+
font-size: 0.85em;
|
|
436
|
+
white-space: pre-wrap;
|
|
437
|
+
word-break: break-all;
|
|
438
|
+
color: #d4d4d4;
|
|
439
|
+
margin: 0;
|
|
440
|
+
}
|
|
441
|
+
.node-details h3 { color: #4ec9b0; margin: 0 0 12px 0; font-size: 1.1em; }
|
|
442
|
+
.detail-row { margin: 6px 0; font-size: 0.95em; }
|
|
443
|
+
.detail-label { color: #808080; display: inline-block; width: 120px; }
|
|
444
|
+
.detail-value { color: #d4d4d4; }
|
|
445
|
+
.error-state {
|
|
446
|
+
text-align: center;
|
|
447
|
+
padding: 60px;
|
|
448
|
+
color: #f48771;
|
|
449
|
+
}
|
|
450
|
+
.loading-state {
|
|
451
|
+
text-align: center;
|
|
452
|
+
padding: 60px;
|
|
453
|
+
color: #808080;
|
|
454
|
+
}
|
|
455
|
+
</style>
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
<div class="container">
|
|
459
|
+
<a href="/" class="back-link">← Back to search</a>
|
|
460
|
+
<h1>IR Dependency Graph</h1>
|
|
461
|
+
|
|
462
|
+
<div class="graph-container">
|
|
463
|
+
<div class="zoom-controls">
|
|
464
|
+
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
|
465
|
+
<button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
|
|
466
|
+
<button class="zoom-btn" onclick="resetZoom()" title="Reset">⟲</button>
|
|
467
|
+
</div>
|
|
468
|
+
<div id="graph">
|
|
469
|
+
<div class="loading-state">Loading graph...</div>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<div id="node-details" class="node-details">
|
|
474
|
+
<h3>Node Details</h3>
|
|
475
|
+
<div id="details-content"></div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div id="def-inspect" class="def-inspect">
|
|
479
|
+
<h3>DefNode Inspect</h3>
|
|
480
|
+
<pre id="inspect-content"></pre>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<script>
|
|
485
|
+
const nodeKey = #{JSON.generate(node_key)};
|
|
486
|
+
let graphData = null;
|
|
487
|
+
|
|
488
|
+
mermaid.initialize({
|
|
489
|
+
startOnLoad: false,
|
|
490
|
+
theme: 'default',
|
|
491
|
+
securityLevel: 'loose',
|
|
492
|
+
flowchart: { curve: 'basis' }
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Zoom functionality
|
|
496
|
+
let currentZoom = 1;
|
|
497
|
+
const ZOOM_STEP = 0.2;
|
|
498
|
+
const MIN_ZOOM = 0.3;
|
|
499
|
+
const MAX_ZOOM = 3;
|
|
500
|
+
|
|
501
|
+
function zoomIn() {
|
|
502
|
+
currentZoom = Math.min(currentZoom + ZOOM_STEP, MAX_ZOOM);
|
|
503
|
+
applyZoom();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function zoomOut() {
|
|
507
|
+
currentZoom = Math.max(currentZoom - ZOOM_STEP, MIN_ZOOM);
|
|
508
|
+
applyZoom();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function resetZoom() {
|
|
512
|
+
currentZoom = 1;
|
|
513
|
+
applyZoom();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function applyZoom() {
|
|
517
|
+
const svg = document.querySelector('#graph svg');
|
|
518
|
+
if (svg) {
|
|
519
|
+
svg.style.transform = `scale(${currentZoom})`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Drag to pan functionality
|
|
524
|
+
let isDragging = false;
|
|
525
|
+
let startX, startY, scrollLeft, scrollTop;
|
|
526
|
+
|
|
527
|
+
function initDrag() {
|
|
528
|
+
const graphDiv = document.getElementById('graph');
|
|
529
|
+
|
|
530
|
+
// Mouse wheel zoom
|
|
531
|
+
graphDiv.addEventListener('wheel', (e) => {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
if (e.deltaY < 0) {
|
|
534
|
+
zoomIn();
|
|
535
|
+
} else {
|
|
536
|
+
zoomOut();
|
|
537
|
+
}
|
|
538
|
+
}, { passive: false });
|
|
539
|
+
|
|
540
|
+
graphDiv.addEventListener('mousedown', (e) => {
|
|
541
|
+
if (e.target.closest('.node')) return; // Don't drag when clicking nodes
|
|
542
|
+
isDragging = true;
|
|
543
|
+
graphDiv.classList.add('dragging');
|
|
544
|
+
startX = e.pageX - graphDiv.offsetLeft;
|
|
545
|
+
startY = e.pageY - graphDiv.offsetTop;
|
|
546
|
+
scrollLeft = graphDiv.scrollLeft;
|
|
547
|
+
scrollTop = graphDiv.scrollTop;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
graphDiv.addEventListener('mouseleave', () => {
|
|
551
|
+
isDragging = false;
|
|
552
|
+
graphDiv.classList.remove('dragging');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
graphDiv.addEventListener('mouseup', () => {
|
|
556
|
+
isDragging = false;
|
|
557
|
+
graphDiv.classList.remove('dragging');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
graphDiv.addEventListener('mousemove', (e) => {
|
|
561
|
+
if (!isDragging) return;
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
const x = e.pageX - graphDiv.offsetLeft;
|
|
564
|
+
const y = e.pageY - graphDiv.offsetTop;
|
|
565
|
+
const walkX = (x - startX) * 1.5;
|
|
566
|
+
const walkY = (y - startY) * 1.5;
|
|
567
|
+
graphDiv.scrollLeft = scrollLeft - walkX;
|
|
568
|
+
graphDiv.scrollTop = scrollTop - walkY;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function loadGraph() {
|
|
573
|
+
const graphDiv = document.getElementById('graph');
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const response = await fetch(`/api/graph?node_key=${encodeURIComponent(nodeKey)}`);
|
|
577
|
+
const data = await response.json();
|
|
578
|
+
|
|
579
|
+
if (data.error) {
|
|
580
|
+
graphDiv.innerHTML = `<div class="error-state">Error: ${data.error}</div>`;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
graphData = data;
|
|
585
|
+
renderGraph();
|
|
586
|
+
showDefInspect(data.def_node_inspect);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
graphDiv.innerHTML = `<div class="error-state">Failed to load graph: ${error.message}</div>`;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function showDefInspect(inspectStr) {
|
|
593
|
+
const inspectDiv = document.getElementById('def-inspect');
|
|
594
|
+
const contentPre = document.getElementById('inspect-content');
|
|
595
|
+
if (inspectStr) {
|
|
596
|
+
contentPre.textContent = inspectStr;
|
|
597
|
+
inspectDiv.style.display = 'block';
|
|
598
|
+
} else {
|
|
599
|
+
inspectDiv.style.display = 'none';
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function renderGraph() {
|
|
604
|
+
const mermaidCode = generateMermaidCode(graphData);
|
|
605
|
+
|
|
606
|
+
const graphDiv = document.getElementById('graph');
|
|
607
|
+
graphDiv.innerHTML = '';
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const { svg } = await mermaid.render('mermaid-graph', mermaidCode);
|
|
611
|
+
graphDiv.innerHTML = svg;
|
|
612
|
+
// Add click handlers to nodes
|
|
613
|
+
addNodeClickHandlers();
|
|
614
|
+
} catch (error) {
|
|
615
|
+
graphDiv.innerHTML = `<div class="error-state">Failed to render graph: ${error.message}</div>`;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function generateMermaidCode(data) {
|
|
620
|
+
let code = `graph BT\\n`;
|
|
621
|
+
|
|
622
|
+
// Add class definitions for styling
|
|
623
|
+
code += ` classDef defNode fill:#569cd6,stroke:#333,color:#fff\\n`;
|
|
624
|
+
code += ` classDef callNode fill:#a3be8c,stroke:#333,color:#000\\n`;
|
|
625
|
+
code += ` classDef varWriteNode fill:#d08770,stroke:#333,color:#fff\\n`;
|
|
626
|
+
code += ` classDef varReadNode fill:#e5ac6b,stroke:#333,color:#000\\n`;
|
|
627
|
+
code += ` classDef paramNode fill:#b48ead,stroke:#333,color:#fff\\n`;
|
|
628
|
+
code += ` classDef literalNode fill:#808080,stroke:#333,color:#fff\\n`;
|
|
629
|
+
code += ` classDef mergeNode fill:#ebcb8b,stroke:#333,color:#000\\n`;
|
|
630
|
+
code += ` classDef blockParamNode fill:#88c0d0,stroke:#333,color:#000\\n`;
|
|
631
|
+
code += ` classDef returnNode fill:#c586c0,stroke:#333,color:#fff\\n`;
|
|
632
|
+
code += ` classDef constNode fill:#4ec9b0,stroke:#333,color:#000\\n`;
|
|
633
|
+
code += ` classDef otherNode fill:#4c566a,stroke:#333,color:#fff\\n`;
|
|
634
|
+
|
|
635
|
+
// Categorize nodes
|
|
636
|
+
const defNode = data.nodes.find(n => n.type === 'DefNode');
|
|
637
|
+
const defNodeKey = defNode?.key;
|
|
638
|
+
const paramNodes = data.nodes.filter(n => n.type === 'ParamNode');
|
|
639
|
+
const callNodes = data.nodes.filter(n => n.type === 'CallNode');
|
|
640
|
+
|
|
641
|
+
// Build edge lookup: which nodes point to which
|
|
642
|
+
const edgesFrom = new Map(); // from -> [to, ...]
|
|
643
|
+
data.edges.forEach(edge => {
|
|
644
|
+
if (!edgesFrom.has(edge.from)) edgesFrom.set(edge.from, []);
|
|
645
|
+
edgesFrom.get(edge.from).push(edge.to);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Track nodes already rendered in subgraphs
|
|
649
|
+
const renderedInSubgraph = new Set();
|
|
650
|
+
|
|
651
|
+
// 1. Add DefNode at top
|
|
652
|
+
if (defNode) {
|
|
653
|
+
const id = sanitizeId(defNode.key);
|
|
654
|
+
const label = formatNodeLabel(defNode);
|
|
655
|
+
code += ` ${id}["${label}"]:::defNode\\n`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 2. Add params subgraph
|
|
659
|
+
if (paramNodes.length > 0) {
|
|
660
|
+
code += ` subgraph params\\n`;
|
|
661
|
+
code += ` direction TB\\n`;
|
|
662
|
+
paramNodes.forEach(node => {
|
|
663
|
+
const id = sanitizeId(node.key);
|
|
664
|
+
const label = formatNodeLabel(node);
|
|
665
|
+
code += ` ${id}["${label}"]:::paramNode\\n`;
|
|
666
|
+
renderedInSubgraph.add(node.key);
|
|
667
|
+
});
|
|
668
|
+
code += ` end\\n`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// 3. Add CallNode subgraphs with receiver and args
|
|
672
|
+
callNodes.forEach(callNode => {
|
|
673
|
+
const deps = edgesFrom.get(callNode.key) || [];
|
|
674
|
+
if (deps.length === 0) {
|
|
675
|
+
// No dependencies, render as simple node
|
|
676
|
+
const id = sanitizeId(callNode.key);
|
|
677
|
+
const label = formatNodeLabel(callNode);
|
|
678
|
+
code += ` ${id}["${label}"]:::callNode\\n`;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Create subgraph for call with its dependencies
|
|
683
|
+
const subgraphId = sanitizeId(callNode.key) + '_sub';
|
|
684
|
+
const receiver = callNode.details?.receiver || '';
|
|
685
|
+
const method = callNode.details?.method || '';
|
|
686
|
+
const subgraphLabel = receiver ? `${receiver}.${method}` : method;
|
|
687
|
+
code += ` subgraph ${subgraphId} ["${escapeForMermaid(subgraphLabel)}"]\\n`;
|
|
688
|
+
code += ` direction TB\\n`;
|
|
689
|
+
|
|
690
|
+
// Add CallNode itself
|
|
691
|
+
const callId = sanitizeId(callNode.key);
|
|
692
|
+
const callLabel = formatNodeLabel(callNode);
|
|
693
|
+
code += ` ${callId}["${callLabel}"]:::callNode\\n`;
|
|
694
|
+
|
|
695
|
+
// Add direct dependencies (receiver, args) inside subgraph
|
|
696
|
+
deps.forEach(depKey => {
|
|
697
|
+
if (renderedInSubgraph.has(depKey)) return;
|
|
698
|
+
const depNode = data.nodes.find(n => n.key === depKey);
|
|
699
|
+
if (depNode && depNode.type !== 'DefNode' && depNode.type !== 'ParamNode') {
|
|
700
|
+
const depId = sanitizeId(depKey);
|
|
701
|
+
const depLabel = formatNodeLabel(depNode);
|
|
702
|
+
const depStyle = getNodeStyleClass(depNode.type, depNode);
|
|
703
|
+
code += ` ${depId}["${depLabel}"]:::${depStyle}\\n`;
|
|
704
|
+
renderedInSubgraph.add(depKey);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
code += ` end\\n`;
|
|
709
|
+
renderedInSubgraph.add(callNode.key);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// 4. Add remaining nodes (not in any subgraph)
|
|
713
|
+
data.nodes.forEach(node => {
|
|
714
|
+
if (node.type === 'DefNode') return; // Already added
|
|
715
|
+
if (renderedInSubgraph.has(node.key)) return;
|
|
716
|
+
const id = sanitizeId(node.key);
|
|
717
|
+
const label = formatNodeLabel(node);
|
|
718
|
+
const styleClass = getNodeStyleClass(node.type, node);
|
|
719
|
+
code += ` ${id}["${label}"]:::${styleClass}\\n`;
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// 5. Add edges (reversed direction: to --> from for BT layout)
|
|
723
|
+
// In IR: from depends on to, so arrow should be to --> from
|
|
724
|
+
data.edges.forEach(edge => {
|
|
725
|
+
const fromId = sanitizeId(edge.from);
|
|
726
|
+
const toId = sanitizeId(edge.to);
|
|
727
|
+
code += ` ${toId} --> ${fromId}\\n`;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return code;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function addNodeClickHandlers() {
|
|
734
|
+
const nodes = document.querySelectorAll('#graph .node');
|
|
735
|
+
nodes.forEach(nodeEl => {
|
|
736
|
+
nodeEl.style.cursor = 'pointer';
|
|
737
|
+
nodeEl.addEventListener('click', () => {
|
|
738
|
+
const nodeId = nodeEl.id;
|
|
739
|
+
const nodeData = graphData.nodes.find(n => sanitizeId(n.key) === nodeId);
|
|
740
|
+
if (nodeData) showNodeDetails(nodeData);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function showNodeDetails(node) {
|
|
746
|
+
const detailsDiv = document.getElementById('node-details');
|
|
747
|
+
const contentDiv = document.getElementById('details-content');
|
|
748
|
+
|
|
749
|
+
let html = `<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">${node.type}</span></div>`;
|
|
750
|
+
html += `<div class="detail-row"><span class="detail-label">Key:</span><span class="detail-value" style="font-size:0.85em">${node.key}</span></div>`;
|
|
751
|
+
html += `<div class="detail-row"><span class="detail-label">Line:</span><span class="detail-value">${node.line || '-'}</span></div>`;
|
|
752
|
+
html += `<div class="detail-row"><span class="detail-label">Inferred Type:</span><span class="detail-value">${node.inferred_type || 'Unknown'}</span></div>`;
|
|
753
|
+
|
|
754
|
+
if (node.details) {
|
|
755
|
+
html += '<hr style="border-color:#3e3e3e;margin:12px 0">';
|
|
756
|
+
Object.entries(node.details).forEach(([key, value]) => {
|
|
757
|
+
const displayValue = Array.isArray(value) ? value.join(', ') || '(none)' :
|
|
758
|
+
typeof value === 'object' ? JSON.stringify(value) : value;
|
|
759
|
+
html += `<div class="detail-row"><span class="detail-label">${key}:</span><span class="detail-value">${displayValue}</span></div>`;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
contentDiv.innerHTML = html;
|
|
764
|
+
detailsDiv.classList.add('visible');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function formatNodeLabel(node) {
|
|
768
|
+
const d = node.details || {};
|
|
769
|
+
|
|
770
|
+
// Format based on node type
|
|
771
|
+
if (node.type === 'DefNode') {
|
|
772
|
+
// Show full method signature: def name(param: Type, ...) -> ReturnType
|
|
773
|
+
const params = (d.param_signatures || []).join(', ');
|
|
774
|
+
const returnType = node.inferred_type || 'untyped';
|
|
775
|
+
return escapeForMermaid(`def ${d.name}(${params})\\n-> ${returnType}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (node.type === 'ParamNode') {
|
|
779
|
+
// param_name: Type
|
|
780
|
+
const type = node.inferred_type || 'Unknown';
|
|
781
|
+
return escapeForMermaid(`${d.name}: ${type}\\n(${d.kind} param)`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (node.type.endsWith('WriteNode')) {
|
|
785
|
+
// var_name: Type (name already includes @ or @@ prefix)
|
|
786
|
+
const type = node.inferred_type || 'Unknown';
|
|
787
|
+
return escapeForMermaid(`${d.name}: ${type}\\n(write, L${node.line})`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (node.type.endsWith('ReadNode')) {
|
|
791
|
+
// var_name: Type (name already includes @ or @@ prefix)
|
|
792
|
+
const type = node.inferred_type || 'Unknown';
|
|
793
|
+
return escapeForMermaid(`${d.name}: ${type}\\n(read, L${node.line})`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (node.type === 'CallNode') {
|
|
797
|
+
const type = node.inferred_type && node.inferred_type !== 'Unknown' ? ` -> ${node.inferred_type}` : '';
|
|
798
|
+
const block = d.has_block ? ' { }' : '';
|
|
799
|
+
const receiver = d.receiver ? `${d.receiver}.` : '';
|
|
800
|
+
return escapeForMermaid(`${receiver}${d.method}${block}${type}\\n(L${node.line})`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (node.type === 'LiteralNode') {
|
|
804
|
+
return escapeForMermaid(`${d.literal_type}\\n(L${node.line})`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (node.type === 'MergeNode') {
|
|
808
|
+
return escapeForMermaid(`Merge (${d.branches_count} branches)\\n(L${node.line})`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Default format for other nodes
|
|
812
|
+
let label = node.type;
|
|
813
|
+
if (d.name) label += `: ${d.name}`;
|
|
814
|
+
else if (d.method) label += `: ${d.method}`;
|
|
815
|
+
else if (d.index !== undefined) label += ` [${d.index}]`;
|
|
816
|
+
if (node.line) label += ` (L${node.line})`;
|
|
817
|
+
if (node.inferred_type && node.inferred_type !== 'Unknown') {
|
|
818
|
+
label += `\\n-> ${node.inferred_type}`;
|
|
819
|
+
}
|
|
820
|
+
return escapeForMermaid(label);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function escapeForMermaid(text) {
|
|
824
|
+
return text
|
|
825
|
+
.replace(/"/g, "'")
|
|
826
|
+
.replace(/\\[/g, '#91;')
|
|
827
|
+
.replace(/\\]/g, '#93;')
|
|
828
|
+
.replace(/</g, '#lt;')
|
|
829
|
+
.replace(/>/g, '#gt;');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function getNodeStyleClass(nodeType, node) {
|
|
833
|
+
if (nodeType.endsWith('WriteNode')) return 'varWriteNode';
|
|
834
|
+
if (nodeType.endsWith('ReadNode')) return 'varReadNode';
|
|
835
|
+
const styles = {
|
|
836
|
+
DefNode: 'defNode',
|
|
837
|
+
CallNode: 'callNode',
|
|
838
|
+
ParamNode: 'paramNode',
|
|
839
|
+
LiteralNode: 'literalNode',
|
|
840
|
+
MergeNode: 'mergeNode',
|
|
841
|
+
BlockParamSlot: 'blockParamNode'
|
|
842
|
+
};
|
|
843
|
+
return styles[nodeType] || 'otherNode';
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function sanitizeId(key) {
|
|
847
|
+
return 'n_' + key.replace(/[^a-zA-Z0-9]/g, '_');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Initial load
|
|
851
|
+
loadGraph();
|
|
852
|
+
initDrag();
|
|
853
|
+
</script>
|
|
854
|
+
</body>
|
|
855
|
+
</html>
|
|
856
|
+
HTML
|
|
857
|
+
}
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
end
|