megrez 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 85a6c24732dbeeb7c806452f4d963df6c0e1d4aa33faa2b45961aeb27c737f03
4
+ data.tar.gz: b742be2cdcd1d743185f2fa3f3eabf0224f1bc94f00f08ea6eaaf35631ac5eb6
5
+ SHA512:
6
+ metadata.gz: 6e1fe32a3fea8f74156ec480ec0fe60c43098b7c5748a3896e6dc13436416d06a4822f87e82b8ecac82698f6b5166c16e2ae01bcb5a1ec331eed586d7e1feed0
7
+ data.tar.gz: 456406256f220f0a3faecb88d8ea49acc07dd15284a3bc1c8e90e2e74888a688798a084609012e2794f833c54826b15239cca1bf22099b30390095be57409f5b
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-16
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Megrez
2
+
3
+ Megrez is a pure Ruby Debug Adapter Protocol (DAP) client. It owns adapter
4
+ transport, protocol validation, session state, breakpoints, execution control,
5
+ and lazy variable expansion without depending on an editor or UI toolkit.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem "megrez"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ require "megrez"
17
+
18
+ session = Megrez::Session.stdio(command: ["path/to/debug-adapter", "--stdio"])
19
+ session.on(:stopped) { |event| puts "stopped: #{event.fetch("reason")}" }
20
+ initialized = Queue.new
21
+ session.on(:initialized) { initialized << true }
22
+ session.start(adapter_id: "my-adapter")
23
+ launch = session.launch("program" => File.expand_path("app.rb"))
24
+ initialized.pop
25
+ session.set_breakpoints("app.rb", [
26
+ Megrez::SourceBreakpoint.new(
27
+ line: 12, column: nil, condition: nil, hit_condition: nil,
28
+ log_message: nil
29
+ )
30
+ ]).await(timeout: 5)
31
+ session.configuration_done.await(timeout: 5)
32
+ launch.await(timeout: 5)
33
+ ```
34
+
35
+ Requests return `Megrez::Future`; call `await(timeout:)` where a blocking
36
+ result is needed. Cancelling a future sends the DAP `cancel` request and
37
+ rejects the local wait. Event callbacks are delivered in adapter arrival order.
38
+
39
+ TCP adapters are supported with `Megrez::Session.tcp(host:, port:)`. Adapter
40
+ reverse requests such as `runInTerminal` and `startDebugging` can be handled
41
+ with `on_request`:
42
+
43
+ ```ruby
44
+ session.on_request("startDebugging") { |arguments| start_child(arguments) }
45
+ ```
46
+
47
+ Register reverse-request handlers before `start`; Megrez advertises only the
48
+ requests that have handlers.
49
+
50
+ Values returned as `variables_reference` embed the current stop generation.
51
+ Passing a value from an earlier stop to `variables` or `set_variable` raises
52
+ `Megrez::Error` instead of querying stale adapter state.
53
+
54
+ ## Conformance
55
+
56
+ The bundled fake adapter is exercised by default. Set
57
+ `MEGREZ_ADAPTERS=ruby` to additionally run the installed `rdbg` conformance
58
+ scenario.
59
+
60
+ ```sh
61
+ bundle install
62
+ bundle exec rake test
63
+ bundle exec rbs -I sig -r stringio validate
64
+ BUDGET=1 bundle exec rake bench
65
+ gem build --strict megrez.gemspec
66
+ ```
67
+
68
+ Adapters are external programs and should be treated as untrusted input.
69
+ Megrez does not invoke a shell, caps frame/header/pending-request sizes, and
70
+ bounds retained stderr and callback errors. TCP is unencrypted; use it only on
71
+ a trusted interface or inside a secure tunnel.
72
+
73
+ ## License
74
+
75
+ Megrez is available under the MIT License.
@@ -0,0 +1,16 @@
1
+ # ADR NNN: Implementation decision title
2
+
3
+ - Status: Proposed
4
+ - Date: YYYY-MM-DD
5
+
6
+ ## Context
7
+
8
+ Describe the concrete implementation question and its constraints.
9
+
10
+ ## Decision
11
+
12
+ Describe the durable boundary or architecture choice.
13
+
14
+ ## Consequences
15
+
16
+ Describe the important trade-offs and when to revisit the decision.
@@ -0,0 +1,20 @@
1
+ # ADR 001: Encode stop generations in variable references
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-09-15
5
+
6
+ ## Context
7
+
8
+ DAP variable references are valid only while execution remains stopped. An
9
+ adapter may reuse the same integer after the next stop, so retaining only a
10
+ set of seen integers cannot reliably identify stale UI state.
11
+
12
+ ## Decision
13
+
14
+ Expose zero unchanged and encode the current stop generation with every
15
+ positive adapter reference. Decode and verify it before sending requests.
16
+
17
+ ## Consequences
18
+
19
+ References remain integers and stale values are rejected deterministically.
20
+ They are opaque handles; callers must not interpret their numeric value.
@@ -0,0 +1,3 @@
1
+ # Architecture decision records
2
+
3
+ These records document Megrez's durable boundaries.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megrez
4
+ class Future
5
+ class Subscription
6
+ def initialize(&detach) = @detach = detach
7
+
8
+ def detach
9
+ callback, @detach = @detach, nil
10
+ callback&.call
11
+ end
12
+ end
13
+
14
+ attr_reader :id, :callback_errors
15
+
16
+ def initialize(id, on_error: nil, &cancel)
17
+ @id = id
18
+ @cancel_callback = cancel
19
+ @on_error = on_error
20
+ @lock = Mutex.new
21
+ @ready = ConditionVariable.new
22
+ @callbacks = []
23
+ @callback_errors = []
24
+ end
25
+
26
+ def fulfill(value = nil, error: nil)
27
+ callbacks = @lock.synchronize do
28
+ return if @done
29
+
30
+ @value = value
31
+ @error = error
32
+ @done = true
33
+ @ready.broadcast
34
+ saved, @callbacks = @callbacks, []
35
+ saved
36
+ end
37
+ callbacks.each { |callback| invoke(callback, @value, @error) }
38
+ self
39
+ end
40
+
41
+ def then(&callback)
42
+ raise ArgumentError, "callback required" unless callback
43
+
44
+ ready = @lock.synchronize do
45
+ @callbacks << callback unless @done
46
+ @done
47
+ end
48
+ invoke(callback, @value, @error) if ready
49
+ self
50
+ end
51
+
52
+ def on_complete(&callback)
53
+ raise ArgumentError, "callback required" unless callback
54
+
55
+ self.then(&callback)
56
+ Subscription.new { @lock.synchronize { @callbacks.delete(callback) } }
57
+ end
58
+
59
+ def done? = @lock.synchronize { !!@done }
60
+
61
+ def await(timeout: nil)
62
+ valid = timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout >= 0)
63
+ raise ArgumentError, "timeout must be finite and nonnegative" unless valid
64
+
65
+ deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
66
+ @lock.synchronize do
67
+ until @done
68
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
69
+ raise Timeout, "DAP request #{@id} timed out" if remaining && remaining <= 0
70
+
71
+ @ready.wait(@lock, remaining)
72
+ end
73
+ raise @error if @error
74
+
75
+ @value
76
+ end
77
+ rescue Timeout
78
+ cancel
79
+ raise
80
+ end
81
+
82
+ def cancel
83
+ callback = @lock.synchronize do
84
+ return false if @done || @cancelling
85
+
86
+ @cancelling = true
87
+ @cancel_callback
88
+ end
89
+ invoke(->(*) { callback.call(@id) }, nil, nil) if callback
90
+ fulfill(error: Cancelled.new("DAP request #{@id} cancelled"))
91
+ true
92
+ end
93
+
94
+ private
95
+
96
+ def invoke(callback, value, error)
97
+ callback.call(value, error)
98
+ rescue StandardError => callback_error
99
+ bounded = Error.new("#{callback_error.class}: #{callback_error.message}".scrub.byteslice(0, 2048).scrub(""))
100
+ @lock.synchronize do
101
+ @callback_errors << bounded
102
+ @callback_errors.shift if @callback_errors.length > 32
103
+ end
104
+ begin
105
+ @on_error&.call(bounded)
106
+ rescue StandardError
107
+ nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megrez
4
+ module Protocol
5
+ MAX_COLLECTION = 100_000
6
+ MAX_DEPTH = 64
7
+
8
+ module_function
9
+
10
+ def validate_message(message)
11
+ raise Error, "DAP message must be an object" unless message.is_a?(Hash)
12
+
13
+ uint(message["seq"], "DAP seq", positive: true)
14
+ case message["type"]
15
+ when "request" then validate_request(message)
16
+ when "response" then validate_response(message)
17
+ when "event" then validate_event(message)
18
+ else raise Error, "invalid DAP message type"
19
+ end
20
+ message
21
+ end
22
+
23
+ def validate_outbound(value)
24
+ validate_json(value, 0)
25
+ value
26
+ end
27
+
28
+ def validate_request(message)
29
+ string(message["command"], "DAP command", empty: false, max: 256)
30
+ validate_json(object(message["arguments"], "DAP request arguments"), 0) if message.key?("arguments")
31
+ forbidden = %w[request_seq success event]
32
+ raise Error, "invalid DAP request fields" if forbidden.any? { |key| message.key?(key) }
33
+ end
34
+
35
+ def validate_response(message)
36
+ uint(message["request_seq"], "DAP request_seq", positive: true)
37
+ boolean(message["success"], "DAP response success")
38
+ string(message["command"], "DAP response command", empty: false, max: 256)
39
+ string(message["message"], "DAP response message", max: 4096) if message.key?("message")
40
+ validate_json(object(message["body"], "DAP response body"), 0) if message.key?("body")
41
+ raise Error, "invalid DAP response fields" if message.key?("event")
42
+ end
43
+
44
+ def validate_event(message)
45
+ string(message["event"], "DAP event", empty: false, max: 256)
46
+ validate_json(object(message["body"], "DAP event body"), 0) if message.key?("body")
47
+ forbidden = %w[request_seq success command]
48
+ raise Error, "invalid DAP event fields" if forbidden.any? { |key| message.key?(key) }
49
+ end
50
+
51
+ def validate_json(value, depth)
52
+ raise Error, "DAP value is nested too deeply" if depth > MAX_DEPTH
53
+
54
+ case value
55
+ when nil, true, false, Integer, String
56
+ string(value, "DAP string") if value.is_a?(String)
57
+ when Float
58
+ raise Error, "DAP number must be finite" unless value.finite?
59
+ when Array
60
+ collection(value, "DAP array").each { |item| validate_json(item, depth + 1) }
61
+ when Hash
62
+ value.each do |key, item|
63
+ raise Error, "DAP object keys must be strings or symbols" unless key.is_a?(String) || key.is_a?(Symbol)
64
+
65
+ string(key.to_s, "DAP object key", empty: false, max: 1024)
66
+ validate_json(item, depth + 1)
67
+ end
68
+ else
69
+ raise Error, "unsupported DAP value: #{value.class}"
70
+ end
71
+ end
72
+
73
+ def object(value, name)
74
+ raise Error, "#{name} must be an object" unless value.is_a?(Hash)
75
+
76
+ value
77
+ end
78
+
79
+ def collection(value, name)
80
+ raise Error, "#{name} must be an array" unless value.is_a?(Array)
81
+ raise Error, "#{name} has too many elements" if value.length > MAX_COLLECTION
82
+
83
+ value
84
+ end
85
+
86
+ def string(value, name, empty: true, max: 1 << 20)
87
+ valid = value.is_a?(String) && value.encoding != Encoding::BINARY && value.valid_encoding? && !value.include?("\0")
88
+ valid &&= !value.empty? unless empty
89
+ valid &&= value.bytesize <= max
90
+ raise Error, "#{name} must be a valid UTF-8 string" unless valid
91
+
92
+ value
93
+ end
94
+
95
+ def integer(value, name)
96
+ raise Error, "#{name} must be an integer" unless value.is_a?(Integer)
97
+
98
+ value
99
+ end
100
+
101
+ def uint(value, name, positive: false)
102
+ integer(value, name)
103
+ minimum = positive ? 1 : 0
104
+ raise Error, "#{name} must be at least #{minimum}" if value < minimum
105
+
106
+ value
107
+ end
108
+
109
+ def boolean(value, name)
110
+ raise Error, "#{name} must be boolean" unless value == true || value == false
111
+
112
+ value
113
+ end
114
+
115
+ def optional_string(value, name, **options)
116
+ value.nil? ? nil : string(value, name, **options)
117
+ end
118
+
119
+ def optional_uint(value, name)
120
+ value.nil? ? nil : uint(value, name)
121
+ end
122
+
123
+ def deep_freeze(value)
124
+ case value
125
+ when Hash
126
+ value.each { |key, item| deep_freeze(key); deep_freeze(item) }
127
+ when Array
128
+ value.each { |item| deep_freeze(item) }
129
+ end
130
+ value.freeze
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megrez
4
+ module Results
5
+ module_function
6
+
7
+ def capabilities(body)
8
+ Protocol.deep_freeze(Protocol.object(body, "initialize response").dup)
9
+ end
10
+
11
+ def breakpoints(body)
12
+ array(body, "breakpoints").map do |value|
13
+ value = Protocol.object(value, "breakpoint")
14
+ Breakpoint.new(
15
+ id: optional_integer(value["id"], "breakpoint id"),
16
+ verified: Protocol.boolean(value["verified"], "breakpoint verified"),
17
+ source: source(value["source"]),
18
+ line: optional_uint(value["line"], "breakpoint line"),
19
+ column: optional_uint(value["column"], "breakpoint column"),
20
+ message: Protocol.optional_string(value["message"], "breakpoint message")
21
+ )
22
+ end.freeze
23
+ end
24
+
25
+ def threads(body)
26
+ array(body, "threads").map do |value|
27
+ value = Protocol.object(value, "thread")
28
+ ThreadInfo.new(
29
+ id: Protocol.integer(value["id"], "thread id"),
30
+ name: Protocol.string(value["name"], "thread name")
31
+ )
32
+ end.freeze
33
+ end
34
+
35
+ def stack_frames(body)
36
+ array(body, "stackFrames").map do |value|
37
+ value = Protocol.object(value, "stack frame")
38
+ StackFrame.new(
39
+ id: Protocol.integer(value["id"], "stack frame id"),
40
+ name: Protocol.string(value["name"], "stack frame name"),
41
+ source: source(value["source"]),
42
+ line: Protocol.uint(value["line"], "stack frame line"),
43
+ column: Protocol.uint(value["column"], "stack frame column"),
44
+ presentation_hint: Protocol.optional_string(value["presentationHint"], "stack frame presentationHint")
45
+ )
46
+ end.freeze
47
+ end
48
+
49
+ def scopes(body, generation)
50
+ array(body, "scopes").map do |value|
51
+ value = Protocol.object(value, "scope")
52
+ Scope.new(
53
+ name: Protocol.string(value["name"], "scope name"),
54
+ variables_reference: encode_reference(value["variablesReference"], generation),
55
+ expensive: Protocol.boolean(value["expensive"], "scope expensive"),
56
+ presentation_hint: Protocol.optional_string(value["presentationHint"], "scope presentationHint")
57
+ )
58
+ end.freeze
59
+ end
60
+
61
+ def variables(body, generation)
62
+ array(body, "variables").map { |value| variable(value, generation) }.freeze
63
+ end
64
+
65
+ def set_variable(body, generation, name)
66
+ value = Protocol.object(body, "setVariable response").merge("name" => name)
67
+ variable(value, generation)
68
+ end
69
+
70
+ def evaluate(body, generation, expression)
71
+ value = Protocol.object(body, "evaluate response").merge(
72
+ "name" => expression,
73
+ "value" => body["result"]
74
+ )
75
+ variable(value, generation)
76
+ end
77
+
78
+ def completions(body)
79
+ array(body, "targets").map do |value|
80
+ value = Protocol.object(value, "completion target")
81
+ Protocol.string(value["label"], "completion label")
82
+ %w[text sortText type].each do |key|
83
+ Protocol.optional_string(value[key], "completion #{key}") if value.key?(key)
84
+ end
85
+ %w[start length selectionStart selectionLength].each do |key|
86
+ Protocol.optional_uint(value[key], "completion #{key}") if value.key?(key)
87
+ end
88
+ Protocol.deep_freeze(value.dup)
89
+ end.freeze
90
+ end
91
+
92
+ def source_content(body)
93
+ body = Protocol.object(body, "source response")
94
+ Protocol.string(body["content"], "source content")
95
+ Protocol.optional_string(body["mimeType"], "source mimeType") if body.key?("mimeType")
96
+ Protocol.deep_freeze(body.dup)
97
+ end
98
+
99
+ def body(body)
100
+ Protocol.deep_freeze(Protocol.object(body, "DAP response body").dup)
101
+ end
102
+
103
+ def encode_reference(value, generation)
104
+ value = Protocol.uint(value, "variablesReference")
105
+ return 0 if value.zero?
106
+ raise Error, "variablesReference is too large" if value > 0x7fff_ffff
107
+
108
+ (generation << 32) | value
109
+ end
110
+
111
+ def decode_reference(value, generation)
112
+ value = Protocol.uint(value, "variables reference")
113
+ raise Error, "variables reference is not expandable" if value.zero?
114
+
115
+ encoded_generation = value >> 32
116
+ raise Error, "stale variables reference" unless encoded_generation == generation
117
+
118
+ value & 0xffff_ffff
119
+ end
120
+
121
+ def variable(value, generation)
122
+ value = Protocol.object(value, "variable")
123
+ Variable.new(
124
+ name: Protocol.string(value["name"], "variable name"),
125
+ value: Protocol.string(value["value"], "variable value"),
126
+ type: Protocol.optional_string(value["type"], "variable type"),
127
+ variables_reference: encode_reference(value.fetch("variablesReference", 0), generation),
128
+ named_count: optional_uint(value["namedVariables"], "variable namedVariables"),
129
+ indexed_count: optional_uint(value["indexedVariables"], "variable indexedVariables"),
130
+ memory_reference: Protocol.optional_string(value["memoryReference"], "variable memoryReference")
131
+ )
132
+ end
133
+
134
+ def source(value)
135
+ return nil if value.nil?
136
+
137
+ value = Protocol.object(value, "source").dup
138
+ Protocol.optional_string(value["name"], "source name") if value.key?("name")
139
+ Protocol.optional_string(value["path"], "source path") if value.key?("path")
140
+ Protocol.optional_uint(value["sourceReference"], "source reference") if value.key?("sourceReference")
141
+ Protocol.deep_freeze(value)
142
+ end
143
+
144
+ def array(body, key)
145
+ body = Protocol.object(body, "DAP response body")
146
+ Protocol.collection(body[key], key)
147
+ end
148
+
149
+ def optional_uint(value, name) = value.nil? ? nil : Protocol.uint(value, name)
150
+ def optional_integer(value, name) = value.nil? ? nil : Protocol.integer(value, name)
151
+ end
152
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Megrez
4
+ class Session
5
+ def threads
6
+ send_request("threads", {}, states: %i[configuring running stopped]) { |body| Results.threads(body) }
7
+ end
8
+
9
+ def stack_trace(thread_id, start: 0, levels: 20)
10
+ thread_id = Protocol.integer(thread_id, "thread id")
11
+ start = Protocol.uint(start, "stack start")
12
+ levels = Protocol.uint(levels, "stack levels")
13
+ send_request(
14
+ "stackTrace",
15
+ {threadId: thread_id, startFrame: start, levels: levels},
16
+ states: [:stopped]
17
+ ) { |body| Results.stack_frames(body) }
18
+ end
19
+
20
+ def scopes(frame_id)
21
+ frame_id = Protocol.integer(frame_id, "frame id")
22
+ stopped_generation = generation
23
+ send_request("scopes", {frameId: frame_id}, states: [:stopped]) do |body|
24
+ ensure_generation(stopped_generation)
25
+ Results.scopes(body, stopped_generation)
26
+ end
27
+ end
28
+
29
+ def variables(reference, start: nil, count: nil, filter: nil)
30
+ stopped_generation = generation
31
+ arguments = {variablesReference: Results.decode_reference(reference, stopped_generation)}
32
+ arguments[:start] = Protocol.uint(start, "variable start") unless start.nil?
33
+ arguments[:count] = Protocol.uint(count, "variable count") unless count.nil?
34
+ arguments[:filter] = variable_filter(filter) unless filter.nil?
35
+ send_request("variables", arguments, states: [:stopped]) do |body|
36
+ ensure_generation(stopped_generation)
37
+ Results.variables(body, stopped_generation)
38
+ end
39
+ end
40
+
41
+ def set_variable(reference, name, value)
42
+ stopped_generation = generation
43
+ reference = Results.decode_reference(reference, stopped_generation)
44
+ name = Protocol.string(name, "variable name")
45
+ value = Protocol.string(value, "variable value")
46
+ send_request(
47
+ "setVariable",
48
+ {variablesReference: reference, name: name, value: value},
49
+ states: [:stopped]
50
+ ) do |body|
51
+ ensure_generation(stopped_generation)
52
+ Results.set_variable(body, stopped_generation, name)
53
+ end
54
+ end
55
+
56
+ def set_expression(expression, value, frame_id: nil)
57
+ stopped_generation = generation
58
+ expression = Protocol.string(expression, "expression", empty: false)
59
+ value = Protocol.string(value, "expression value")
60
+ arguments = {expression: expression, value: value}
61
+ arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
62
+ send_request("setExpression", arguments, states: [:stopped]) do |body|
63
+ ensure_generation(stopped_generation)
64
+ Results.set_variable(body, stopped_generation, expression)
65
+ end
66
+ end
67
+
68
+ def evaluate(expression, frame_id: nil, context: "repl")
69
+ stopped_generation = generation
70
+ expression = Protocol.string(expression, "expression")
71
+ context = Protocol.string(context, "evaluation context", empty: false)
72
+ arguments = {expression: expression, context: context}
73
+ arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
74
+ send_request("evaluate", arguments, states: [:stopped]) do |body|
75
+ ensure_generation(stopped_generation)
76
+ Results.evaluate(body, stopped_generation, expression)
77
+ end
78
+ end
79
+
80
+ def completions(text, column, frame_id: nil)
81
+ text = Protocol.string(text, "completion text")
82
+ column = Protocol.uint(column, "completion column")
83
+ arguments = {text: text, column: column}
84
+ arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
85
+ send_request("completions", arguments, states: [:stopped]) { |body| Results.completions(body) }
86
+ end
87
+
88
+ def source(reference)
89
+ reference = Protocol.uint(reference, "source reference")
90
+ send_request("source", {sourceReference: reference}, states: %i[configuring running stopped]) do |body|
91
+ Results.source_content(body)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def variable_filter(value)
98
+ value = value.to_s
99
+ raise ArgumentError, "filter must be indexed or named" unless %w[indexed named].include?(value)
100
+
101
+ value
102
+ end
103
+
104
+ def ensure_generation(expected)
105
+ raise Error, "debuggee resumed while request was pending" unless generation == expected && state == :stopped
106
+ end
107
+ end
108
+ end