Skip to content

Commit

Permalink
Interpreter: allow inspect vars when inside a block (#12165)
Browse files Browse the repository at this point in the history
  • Loading branch information
asterite authored Jun 29, 2022
1 parent 632a86b commit 5b10da3
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 105 deletions.
7 changes: 3 additions & 4 deletions src/compiler/crystal/interpreter/compiled_block.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ class Crystal::Repl
# The bytecode to execute the block.
getter instructions : CompiledInstructions

# Local variables for the block (they might reference variables outside of the block)
getter local_vars : LocalVars

# How many bytes occupy the block args
getter args_bytesize : Int32

Expand All @@ -21,8 +18,10 @@ class Crystal::Repl
# What's the byte offset where the local vars of this block end
getter locals_bytesize_end : Int32

# Local variables for the block (they might reference variables outside of the block)
property! local_vars : LocalVars

def initialize(@block : Block,
@local_vars : LocalVars,
@args_bytesize : Int32,
@locals_bytesize_start : Int32,
@locals_bytesize_end : Int32)
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/crystal/interpreter/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2030,7 +2030,7 @@ class Crystal::Repl::Compiler < Crystal::Visitor

block_args_bytesize = block.args.sum { |arg| aligned_sizeof_type(arg) }

compiled_block = CompiledBlock.new(block, @local_vars,
compiled_block = CompiledBlock.new(block,
args_bytesize: block_args_bytesize,
locals_bytesize_start: bytesize_before_block_local_vars,
locals_bytesize_end: bytesize_after_block_local_vars,
Expand All @@ -2047,6 +2047,10 @@ class Crystal::Repl::Compiler < Crystal::Visitor

compiler.compile_block(block, target_def, @closure_context)

# Keep a copy of the local vars before exiting the block.
# Otherwise we'll lose reference to the block's vars (useful for pry)
compiled_block.local_vars = @local_vars.dup

{% if Debug::DECOMPILE %}
puts "=== #{target_def.owner}##{target_def.name}#block ==="
puts Disassembler.disassemble(@context, compiled_block.instructions, @local_vars)
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/interpreter/disassembler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ module Crystal::Repl::Disassembler
disassemble(context, compiled_def.instructions, compiled_def.local_vars)
end

def self.disassemble(context : Context, compiled_block : CompiledBlock) : String
disassemble(context, compiled_block.instructions, compiled_block.local_vars)
end

def self.disassemble(context : Context, instructions : CompiledInstructions, local_vars : LocalVars) : String
String.build do |io|
exception_handlers = instructions.exception_handlers
Expand Down
208 changes: 113 additions & 95 deletions src/compiler/crystal/interpreter/interpreter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Crystal::Repl::Interpreter
record CallFrame,
# The CompiledDef related to this call frame
compiled_def : CompiledDef,
# The CompiledBlock related to this call frame, if any
compiled_block : CompiledBlock?,
# Instructions for this frame
instructions : CompiledInstructions,
# The pointer to the current instruction for this call frame.
Expand Down Expand Up @@ -122,9 +124,13 @@ class Crystal::Repl::Interpreter
@compiled_def = nil
end

def initialize(interpreter : Interpreter, compiled_def : CompiledDef, stack : Pointer(UInt8), @block_level : Int32)
def self.new(interpreter : Interpreter, compiled_def : CompiledDef, stack : Pointer(UInt8), block_level : Int32)
new(interpreter, compiled_def, compiled_def.local_vars, stack, block_level)
end

def initialize(interpreter : Interpreter, compiled_def : CompiledDef, local_vars : LocalVars, stack : Pointer(UInt8), @block_level : Int32)
@context = interpreter.context
@local_vars = compiled_def.local_vars.dup
@local_vars = local_vars.dup
@argv = interpreter.@argv

@instructions = CompiledInstructions.new
Expand Down Expand Up @@ -281,6 +287,7 @@ class Crystal::Repl::Interpreter
instructions: instructions,
local_vars: @local_vars,
),
compiled_block: nil,
instructions: instructions,
ip: ip,
stack: stack,
Expand Down Expand Up @@ -527,6 +534,7 @@ class Crystal::Repl::Interpreter

%call_frame = CallFrame.new(
compiled_def: {{compiled_def}},
compiled_block: nil,
instructions: {{compiled_def}}.instructions,
ip: {{compiled_def}}.instructions.instructions.to_unsafe,
# We need to adjust the call stack to start right
Expand Down Expand Up @@ -562,6 +570,7 @@ class Crystal::Repl::Interpreter
%block_caller_frame_index = @call_stack.last.block_caller_frame_index

copied_call_frame = @call_stack[%block_caller_frame_index].copy_with(
compiled_block: {{compiled_block}},
instructions: {{compiled_block}}.instructions,
ip: {{compiled_block}}.instructions.instructions.to_unsafe,
stack: stack,
Expand Down Expand Up @@ -1132,107 +1141,116 @@ class Crystal::Repl::Interpreter
end

private def pry(ip, instructions, stack_bottom, stack)
call_frame = @call_stack.last
compiled_def = call_frame.compiled_def
a_def = compiled_def.def
local_vars = compiled_def.local_vars
offset = (ip - instructions.instructions.to_unsafe).to_i32
node = instructions.nodes[offset]?
pry_node = @pry_node
if node && (location = node.location) && different_node_line?(node, pry_node)
whereami(a_def, location)

# puts
# puts Slice.new(stack_bottom, stack - stack_bottom).hexdump
# puts

# Remember the portion from stack_bottom + local_vars.max_bytesize up to stack
# because it might happen that the child interpreter will overwrite some
# of that if we already have some values in the stack past the local vars
data_size = stack - (stack_bottom + local_vars.max_bytesize)
data = Pointer(Void).malloc(data_size).as(UInt8*)
data.copy_from(stack_bottom + local_vars.max_bytesize, data_size)

gatherer = LocalVarsGatherer.new(location, a_def)
gatherer.gather
meta_vars = gatherer.meta_vars
block_level = gatherer.block_level

main_visitor = MainVisitor.new(
@context.program,
vars: meta_vars,
meta_vars: meta_vars,
typed_def: a_def)
main_visitor.scope = compiled_def.owner
main_visitor.path_lookup = compiled_def.owner # TODO: this is probably not right

interpreter = Interpreter.new(self, compiled_def, stack_bottom, block_level)

while @pry
# TODO: support multi-line expressions

print "pry> "
line = gets
unless line
self.pry = false
break
end

case line
when "continue"
self.pry = false
break
when "step"
@pry_node = node
@pry_max_target_frame = nil
break
when "next"
@pry_node = node
@pry_max_target_frame = @call_stack.last.real_frame_index
break
when "finish"
@pry_node = node
@pry_max_target_frame = @call_stack.last.real_frame_index - 1
break
when "whereami"
whereami(a_def, location)
next
when "*d"
puts compiled_def.local_vars
puts Disassembler.disassemble(@context, compiled_def)
next
when "*s"
puts Slice.new(@stack, stack - @stack).hexdump
next
end
return unless node

begin
parser = Parser.new(
line,
string_pool: @context.program.string_pool,
var_scopes: [interpreter.local_vars.names.to_set],
)
line_node = parser.parse

line_node = @context.program.normalize(line_node)
line_node = @context.program.semantic(line_node, main_visitor: main_visitor)

value = interpreter.interpret(line_node, meta_vars)
puts value.to_s
rescue ex : Crystal::CodeError
ex.color = true
ex.error_trace = true
puts ex
next
rescue ex : Exception
ex.inspect_with_backtrace(STDOUT)
next
end
location = node.location
return unless location

return unless different_node_line?(node, pry_node)

call_frame = @call_stack.last
compiled_def = call_frame.compiled_def
compiled_block = call_frame.compiled_block
local_vars = compiled_block.try(&.local_vars) || compiled_def.local_vars

a_def = compiled_def.def

whereami(a_def, location)

# puts
# puts Slice.new(stack_bottom, stack - stack_bottom).hexdump
# puts

# Remember the portion from stack_bottom + local_vars.max_bytesize up to stack
# because it might happen that the child interpreter will overwrite some
# of that if we already have some values in the stack past the local vars
data_size = stack - (stack_bottom + local_vars.max_bytesize)
data = Pointer(Void).malloc(data_size).as(UInt8*)
data.copy_from(stack_bottom + local_vars.max_bytesize, data_size)

gatherer = LocalVarsGatherer.new(location, a_def)
gatherer.gather
meta_vars = gatherer.meta_vars
block_level = local_vars.block_level

main_visitor = MainVisitor.new(
@context.program,
vars: meta_vars,
meta_vars: meta_vars,
typed_def: a_def)
main_visitor.scope = compiled_def.owner
main_visitor.path_lookup = compiled_def.owner # TODO: this is probably not right

interpreter = Interpreter.new(self, compiled_def, local_vars, stack_bottom, block_level)

while @pry
# TODO: support multi-line expressions

print "pry> "
line = gets
unless line
self.pry = false
break
end

# Restore the stack data in case it tas overwritten
(stack_bottom + local_vars.max_bytesize).copy_from(data, data_size)
case line
when "continue"
self.pry = false
break
when "step"
@pry_node = node
@pry_max_target_frame = nil
break
when "next"
@pry_node = node
@pry_max_target_frame = @call_stack.last.real_frame_index
break
when "finish"
@pry_node = node
@pry_max_target_frame = @call_stack.last.real_frame_index - 1
break
when "whereami"
whereami(a_def, location)
next
when "*d"
puts local_vars
puts Disassembler.disassemble(@context, compiled_block || compiled_def)
next
when "*s"
puts Slice.new(@stack, stack - @stack).hexdump
next
end

begin
parser = Parser.new(
line,
string_pool: @context.program.string_pool,
var_scopes: [meta_vars.keys.to_set],
)
line_node = parser.parse

line_node = @context.program.normalize(line_node)
line_node = @context.program.semantic(line_node, main_visitor: main_visitor)

value = interpreter.interpret(line_node, meta_vars)
puts value.to_s
rescue ex : Crystal::CodeError
ex.color = true
ex.error_trace = true
puts ex
next
rescue ex : Exception
ex.inspect_with_backtrace(STDOUT)
next
end
end

# Restore the stack data in case it tas overwritten
(stack_bottom + local_vars.max_bytesize).copy_from(data, data_size)
end

private def whereami(a_def : Def, location : Location)
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/crystal/interpreter/local_vars.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ require "./repl"
class Crystal::Repl::LocalVars
record Key, name : String, block_level : Int32

getter block_level : Int32

def initialize(@context : Context)
@types = {} of Key => Type
@name_to_index = {} of Key => Int32
Expand Down
5 changes: 0 additions & 5 deletions src/compiler/crystal/interpreter/local_vars_gatherer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ class Crystal::Repl::LocalVarsGatherer < Crystal::Visitor
# in the `initialize` method.
getter meta_vars : MetaVars

# What's the block level the `debugger` is currently in.
getter block_level : Int32

def initialize(@location : Location, @def : Def)
@meta_vars = MetaVars.new
@block_level = 0
end

def gather : Nil
Expand Down Expand Up @@ -91,7 +87,6 @@ class Crystal::Repl::LocalVarsGatherer < Crystal::Visitor

old_block = @block
@block = node
@block_level += 1

node.args.each &.accept self
node.body.accept self
Expand Down

0 comments on commit 5b10da3

Please sign in to comment.