Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 6d0b318

Browse files
committed
Disassemble to YARV
1 parent 0af5e70 commit 6d0b318

File tree

7 files changed

+205
-7
lines changed

7 files changed

+205
-7
lines changed

lib/syntax_tree.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def initialize(start_line:, start_char:, end_line:, end_char:)
7272
@end_char = end_char
7373
end
7474

75+
def lines
76+
start_line..end_line
77+
end
78+
7579
def ==(other)
7680
other.is_a?(Location) && start_line == other.start_line &&
7781
start_char == other.start_char && end_line == other.end_line &&
@@ -3098,12 +3102,14 @@ def to_json(*opts)
30983102
# (nil | Ensure) ensure_clause
30993103
# ) -> BodyStmt
31003104
def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause)
3105+
ending = [ensure_clause, else_clause, rescue_clause, statements].detect(&:itself)
3106+
31013107
BodyStmt.new(
31023108
statements: statements,
31033109
rescue_clause: rescue_clause,
31043110
else_clause: else_clause,
31053111
ensure_clause: ensure_clause,
3106-
location: Location.fixed(line: lineno, char: char_pos)
3112+
location: statements.location.to(ending.location)
31073113
)
31083114
end
31093115

lib/syntax_tree/code_actions.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
class SyntaxTree
4+
class CodeActions
5+
class DisasmAction
6+
attr_reader :line, :node
7+
8+
def initialize(line, node)
9+
@line = line
10+
@node = node
11+
end
12+
13+
def as_json
14+
{
15+
title: "Disasm #{node.name.value}",
16+
command: "syntaxTree.disasm",
17+
arguments: [line, node.name.value]
18+
}
19+
end
20+
end
21+
22+
attr_reader :line, :actions
23+
24+
def initialize(line)
25+
@line = line
26+
@actions = []
27+
end
28+
29+
def disasm(node)
30+
@actions << DisasmAction.new(line, node)
31+
end
32+
33+
def self.find(program, line)
34+
code_actions = new(line)
35+
queue = [program]
36+
37+
until queue.empty?
38+
node = queue.shift
39+
40+
# If we're found a method definition that starts on the given line, then
41+
# we're going to add the disasm code action for that node.
42+
if [Def, DefEndless, Defs].include?(node.class) && node.location.start_line == line
43+
code_actions.disasm(node)
44+
end
45+
46+
# Check if the node covers the given line. If it doesn't, then we just
47+
# bail out and go to the next node in the queue.
48+
next unless node.location.lines.cover?(line)
49+
50+
# Get a list of child nodes and binary search over them to find the
51+
# first child node that covers the given line. It's possible that there
52+
# are multiple, but at least we'll have a handle on one of them.
53+
child_nodes = node.child_nodes.compact
54+
index =
55+
child_nodes.bsearch_index do |child|
56+
if child.location.lines.cover?(line)
57+
0
58+
else
59+
line - child.location.start_line
60+
end
61+
end
62+
63+
# If no valid child was found, then just continue on to the next node in
64+
# the queue.
65+
next unless index
66+
67+
# First, we're going to go backward in our list until we find the start
68+
# of the subset of child nodes that cover the given line. We're going to
69+
# add each one to the queue.
70+
current = index - 1
71+
while current > 0 && child_nodes[current].location.lines.cover?(line)
72+
queue << child_nodes[current]
73+
current -= 1
74+
end
75+
76+
# Next, we're going to go forward in our list until we find the end of
77+
# the subset of child nodes that cover the given line. We'll add each of
78+
# those to the queue as well.
79+
current = index
80+
while current < child_nodes.length && child_nodes[current].location.lines.cover?(line)
81+
queue << child_nodes[current]
82+
current += 1
83+
end
84+
end
85+
86+
code_actions
87+
end
88+
end
89+
end

lib/syntax_tree/language_server.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "json"
55
require "uri"
66

7+
require_relative "code_actions"
78
require_relative "implicits"
89

910
class SyntaxTree
@@ -22,8 +23,8 @@ def run
2223
end
2324

2425
while headers = input.gets("\r\n\r\n")
25-
length = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
26-
request = JSON.parse(length, symbolize_names: true)
26+
source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
27+
request = JSON.parse(source, symbolize_names: true)
2728

2829
case request
2930
in { method: "initialize", id: }
@@ -34,6 +35,8 @@ def run
3435
in { method: "shutdown" }
3536
store.clear
3637
return
38+
in { method: "textDocument/codeAction", id:, params: { textDocument: { uri: }, range: { start: { line: } } } }
39+
write(id: id, result: code_actions(store[uri], line + 1))
3740
in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
3841
store[uri] = text
3942
in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
@@ -42,6 +45,8 @@ def run
4245
store.delete(uri)
4346
in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
4447
write(id: id, result: [format(store[uri])])
48+
in { method: "syntaxTree/disasm", id:, params: { textDocument: { uri:, query: { line:, name: } } } }
49+
write(id: id, result: disasm(store[uri], line.to_i, name))
4550
in { method: "syntaxTree/implicits", id:, params: { textDocument: { uri: } } }
4651
write(id: id, result: implicits(store[uri]))
4752
in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
@@ -60,11 +65,43 @@ def run
6065

6166
def capabilities
6267
{
68+
codeActionProvider: { codeActionsKinds: ["disasm"] },
6369
documentFormattingProvider: true,
64-
textDocumentSync: { change: 1, openClose: true },
70+
textDocumentSync: { change: 1, openClose: true }
6571
}
6672
end
6773

74+
def code_actions(source, line)
75+
actions = CodeActions.find(SyntaxTree.parse(source), line).actions
76+
log("Found #{actions.length} actions on line #{line}")
77+
78+
actions.map(&:as_json)
79+
end
80+
81+
def disasm(source, line, name)
82+
actions = CodeActions.find(SyntaxTree.parse(source), line).actions
83+
log("Disassembling #{name.inspect} on line #{line.inspect}")
84+
85+
matched = actions.detect { |action| action.is_a?(CodeActions::DisasmAction) && action.node.name.value == name }
86+
return "Unable to find method: #{name}" unless matched
87+
88+
# First, get an instruction sequence that encompasses the method that
89+
# we're going to disassemble. It will include the method declaration,
90+
# which will be the top instruction sequence.
91+
location = matched.node.location
92+
iseq = RubyVM::InstructionSequence.new(source[location.start_char...location.end_char])
93+
94+
# Next, get the first child. We do this because the parent instruction
95+
# sequence is the method declaration, whereas the first child is the body
96+
# of the method, which is what we're interested in.
97+
method = nil
98+
iseq.each_child { |child| method = child }
99+
100+
# Finally, return the disassembly as a string to the server, which will
101+
# serialize it to JSON and return it back to the client.
102+
method.disasm
103+
end
104+
68105
def format(source)
69106
{
70107
range: {
@@ -75,6 +112,10 @@ def format(source)
75112
}
76113
end
77114

115+
def log(message)
116+
write(method: "window/logMessage", params: { type: 4, message: message })
117+
end
118+
78119
def implicits(source)
79120
implicits = Implicits.find(SyntaxTree.parse(source))
80121
serialize = ->(position, text) { { position: position, text: text } }

vscode/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"onCommand:syntaxTree.stop",
2323
"onCommand:syntaxTree.restart",
2424
"onCommand:syntaxTree.showOutputChannel",
25-
"onCommand:syntaxTree.visualize"
25+
"onCommand:syntaxTree.visualize",
26+
"onCommand:syntaxTree.disasm"
2627
],
2728
"main": "./out/extension",
2829
"contributes": {
@@ -46,6 +47,10 @@
4647
{
4748
"command": "syntaxTree.visualize",
4849
"title": "Syntax Tree: Visualize"
50+
},
51+
{
52+
"command": "syntaxTree.disasm",
53+
"title": "Syntax Tree: Disassemble"
4954
}
5055
],
5156
"colors": [

vscode/src/Disasm.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode";
2+
import { LanguageClient } from "vscode-languageclient/node";
3+
4+
class Disasm implements Disposable, TextDocumentContentProvider {
5+
// The client used to communicate with the language server.
6+
private readonly languageClient: LanguageClient;
7+
8+
// The output channel used for logging for this class. It's given from the
9+
// main file so that it uses the same as the rest of the extension.
10+
private readonly outputChannel: OutputChannel;
11+
12+
// The list of callbacks and objects that should be disposed when an instance
13+
// of Visualize is being disposed.
14+
private readonly disposables: Disposable[];
15+
16+
constructor(languageClient: LanguageClient, outputChannel: OutputChannel) {
17+
this.languageClient = languageClient;
18+
this.outputChannel = outputChannel;
19+
this.disposables = [
20+
commands.registerCommand("syntaxTree.disasm", this.disasm),
21+
workspace.registerTextDocumentContentProvider("syntaxTree.disasm", this)
22+
];
23+
}
24+
25+
dispose() {
26+
this.disposables.forEach((disposable) => disposable.dispose());
27+
}
28+
29+
provideTextDocumentContent(uri: Uri): ProviderResult<string> {
30+
this.outputChannel.appendLine("Requesting disassembly");
31+
32+
const query: Record<string, string> = {};
33+
uri.query.split("&").forEach((pair) => {
34+
const [key, value] = pair.split("=");
35+
query[key] = value;
36+
});
37+
38+
return this.languageClient.sendRequest("syntaxTree/disasm", { textDocument: { uri: uri.path, query } });
39+
}
40+
41+
async disasm(line: number, name: string) {
42+
const document = window.activeTextEditor?.document;
43+
44+
if (document && document.languageId === "ruby" && document.uri.scheme === "file") {
45+
const uri = Uri.parse(`syntaxTree.disasm:${document.uri.toString()}?line=${line}&name=${name}`);
46+
47+
const doc = await workspace.openTextDocument(uri);
48+
languages.setTextDocumentLanguage(doc, "plaintext");
49+
50+
await window.showTextDocument(doc, ViewColumn.Beside, true);
51+
}
52+
}
53+
}
54+
55+
export default Disasm;

vscode/src/Visualize.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Visualize implements Disposable, TextDocumentContentProvider {
1818
this.outputChannel = outputChannel;
1919
this.disposables = [
2020
commands.registerCommand("syntaxTree.visualize", this.visualize),
21-
workspace.registerTextDocumentContentProvider("syntaxTree", this)
21+
workspace.registerTextDocumentContentProvider("syntaxTree.visualize", this)
2222
];
2323
}
2424

@@ -35,7 +35,7 @@ class Visualize implements Disposable, TextDocumentContentProvider {
3535
const document = window.activeTextEditor?.document;
3636

3737
if (document && document.languageId === "ruby" && document.uri.scheme === "file") {
38-
const uri = Uri.parse(`syntaxTree:${document.uri.toString()}`);
38+
const uri = Uri.parse(`syntaxTree.visualize:${document.uri.toString()}`);
3939

4040
const doc = await workspace.openTextDocument(uri);
4141
languages.setTextDocumentLanguage(doc, "plaintext");

vscode/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ExtensionContext, commands, window } from "vscode";
44
import { LanguageClient } from "vscode-languageclient/node";
55

6+
import Disasm from "./Disasm";
67
import Implicits from "./Implicits";
78
import startLanguageClient from "./startLanguageClient";
89
import Visualize from "./Visualize";
@@ -30,6 +31,7 @@ export function activate(context: ExtensionContext) {
3031
await languageClient.onReady();
3132

3233
context.subscriptions.push(
34+
new Disasm(languageClient, outputChannel),
3335
new Implicits(languageClient, outputChannel),
3436
new Visualize(languageClient, outputChannel)
3537
);

0 commit comments

Comments
 (0)