Skip to content

Various updates #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 10, 2023
Merged
Prev Previous commit
WithEnvironment -> WithScope
  • Loading branch information
kddnewton committed Feb 10, 2023
commit 4a6fc77abd4c696b3d38498250ab37e571f27d9a
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ It is built with only standard library dependencies. It additionally ships with
- [visit_methods](#visit_methods)
- [BasicVisitor](#basicvisitor)
- [MutationVisitor](#mutationvisitor)
- [WithEnvironment](#withenvironment)
- [WithScope](#withscope)
- [Language server](#language-server)
- [textDocument/formatting](#textdocumentformatting)
- [textDocument/inlayHint](#textdocumentinlayhint)
Expand Down Expand Up @@ -588,20 +588,18 @@ SyntaxTree::Formatter.format(source, program.accept(visitor))
# => "if (a = 1)\nend\n"
```

### WithEnvironment
### WithScope

The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments
defined inside each environment. A `current_environment` accessor is made available to the request, allowing it to find
all usages and definitions of a local.
The `WithScope` module can be included in visitors to automatically keep track of local variables and arguments defined inside each scope. A `current_scope` accessor is made available to the request, allowing it to find all usages and definitions of a local.

```ruby
class MyVisitor < Visitor
include WithEnvironment
prepend WithScope

def visit_ident(node)
# find_local will return a Local for any local variables or arguments
# present in the current environment or nil if the identifier is not a local
local = current_environment.find_local(node)
local = current_scope.find_local(node)

puts local.type # the type of the local (:variable or :argument)
puts local.definitions # the array of locations where this local is defined
Expand Down
2 changes: 1 addition & 1 deletion lib/syntax_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module SyntaxTree
autoload :PrettyPrintVisitor, "syntax_tree/pretty_print_visitor"
autoload :Search, "syntax_tree/search"
autoload :Translation, "syntax_tree/translation"
autoload :WithEnvironment, "syntax_tree/with_environment"
autoload :WithScope, "syntax_tree/with_scope"
autoload :YARV, "syntax_tree/yarv"

# This holds references to objects that respond to both #parse and #format
Expand Down
107 changes: 50 additions & 57 deletions lib/syntax_tree/with_environment.rb → lib/syntax_tree/with_scope.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# frozen_string_literal: true

module SyntaxTree
# WithEnvironment is a module intended to be included in classes inheriting
# from Visitor. The module overrides a few visit methods to automatically keep
# track of local variables and arguments defined in the current environment.
# WithScope is a module intended to be included in classes inheriting from
# Visitor. The module overrides a few visit methods to automatically keep
# track of local variables and arguments defined in the current scope.
# Example usage:
#
# class MyVisitor < Visitor
# include WithEnvironment
# include WithScope
#
# def visit_ident(node)
# # Check if we're visiting an identifier for an argument, a local
# # variable or something else
# local = current_environment.find_local(node)
# local = current_scope.find_local(node)
#
# if local.type == :argument
# # handle identifiers for arguments
Expand All @@ -24,11 +24,11 @@ module SyntaxTree
# end
# end
#
module WithEnvironment
# The environment class is used to keep track of local variables and
# arguments inside a particular scope
class Environment
# This class tracks the occurrences of a local variable or argument
module WithScope
# The scope class is used to keep track of local variables and arguments
# inside a particular scope.
class Scope
# This class tracks the occurrences of a local variable or argument.
class Local
# [Symbol] The type of the local (e.g. :argument, :variable)
attr_reader :type
Expand All @@ -55,20 +55,20 @@ def add_usage(location)
end
end

# [Integer] a unique identifier for this environment
# [Integer] a unique identifier for this scope
attr_reader :id

# [scope | nil] The parent scope
attr_reader :parent

# [Hash[String, Local]] The local variables and arguments defined in this
# environment
# scope
attr_reader :locals

# [Environment | nil] The parent environment
attr_reader :parent

def initialize(id, parent = nil)
@id = id
@locals = {}
@parent = parent
@locals = {}
end

# Adding a local definition will either insert a new entry in the locals
Expand Down Expand Up @@ -97,7 +97,7 @@ def add_local_usage(identifier, type)
resolve_local(name, type).add_usage(identifier.location)
end

# Try to find the local given its name in this environment or any of its
# Try to find the local given its name in this scope or any of its
# parents.
def find_local(name)
locals[name] || parent&.find_local(name)
Expand All @@ -117,44 +117,35 @@ def resolve_local(name, type)
end
end

attr_reader :current_scope

def initialize(*args, **kwargs, &block)
super
@environment_id = 0
end

def current_environment
@current_environment ||= Environment.new(next_environment_id)
end

def with_new_environment(parent_environment = nil)
previous_environment = @current_environment
@current_environment =
Environment.new(next_environment_id, parent_environment)
yield
ensure
@current_environment = previous_environment
@current_scope = Scope.new(0)
@next_scope_id = 0
end

# Visits for nodes that create new environments, such as classes, modules
# Visits for nodes that create new scopes, such as classes, modules
# and method definitions.
def visit_class(node)
with_new_environment { super }
with_scope { super }
end

def visit_module(node)
with_new_environment { super }
with_scope { super }
end

# When we find a method invocation with a block, only the code that
# happens inside of the block needs a fresh environment. The method
# invocation itself happens in the same environment.
# When we find a method invocation with a block, only the code that happens
# inside of the block needs a fresh scope. The method invocation
# itself happens in the same scope.
def visit_method_add_block(node)
visit(node.call)
with_new_environment(current_environment) { visit(node.block) }
with_scope(current_scope) { visit(node.block) }
end

def visit_def(node)
with_new_environment { super }
with_scope { super }
end

# Visit for keeping track of local arguments, such as method and block
Expand All @@ -163,48 +154,45 @@ def visit_params(node)
add_argument_definitions(node.requireds)

node.posts.each do |param|
current_environment.add_local_definition(param, :argument)
current_scope.add_local_definition(param, :argument)
end

node.keywords.each do |param|
current_environment.add_local_definition(param.first, :argument)
current_scope.add_local_definition(param.first, :argument)
end

node.optionals.each do |param|
current_environment.add_local_definition(param.first, :argument)
current_scope.add_local_definition(param.first, :argument)
end

super
end

def visit_rest_param(node)
name = node.name
current_environment.add_local_definition(name, :argument) if name
current_scope.add_local_definition(name, :argument) if name

super
end

def visit_kwrest_param(node)
name = node.name
current_environment.add_local_definition(name, :argument) if name
current_scope.add_local_definition(name, :argument) if name

super
end

def visit_blockarg(node)
name = node.name
current_environment.add_local_definition(name, :argument) if name
current_scope.add_local_definition(name, :argument) if name

super
end

# Visit for keeping track of local variable definitions
def visit_var_field(node)
value = node.value

if value.is_a?(SyntaxTree::Ident)
current_environment.add_local_definition(value, :variable)
end
current_scope.add_local_definition(value, :variable) if value.is_a?(Ident)

super
end
Expand All @@ -215,12 +203,9 @@ def visit_var_field(node)
def visit_var_ref(node)
value = node.value

if value.is_a?(SyntaxTree::Ident)
definition = current_environment.find_local(value.value)

if definition
current_environment.add_local_usage(value, definition.type)
end
if value.is_a?(Ident)
definition = current_scope.find_local(value.value)
current_scope.add_local_usage(value, definition.type) if definition
end

super
Expand All @@ -233,13 +218,21 @@ def add_argument_definitions(list)
if param.is_a?(SyntaxTree::MLHSParen)
add_argument_definitions(param.contents.parts)
else
current_environment.add_local_definition(param, :argument)
current_scope.add_local_definition(param, :argument)
end
end
end

def next_environment_id
@environment_id += 1
def next_scope_id
@next_scope_id += 1
end

def with_scope(parent_scope = nil)
previous_scope = @current_scope
@current_scope = Scope.new(next_scope_id, parent_scope)
yield
ensure
@current_scope = previous_scope
end
end
end
22 changes: 11 additions & 11 deletions test/with_environment_test.rb → test/with_scope_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
require_relative "test_helper"

module SyntaxTree
class WithEnvironmentTest < Minitest::Test
class WithScopeTest < Minitest::Test
class Collector < Visitor
prepend WithEnvironment
prepend WithScope

attr_reader :arguments, :variables

Expand All @@ -21,22 +21,22 @@ def self.collect(source)
visit_methods do
def visit_ident(node)
value = node.value.delete_suffix(":")
local = current_environment.find_local(node.value)
local = current_scope.find_local(node.value)

case local&.type
when :argument
arguments[[current_environment.id, value]] = local
arguments[[current_scope.id, value]] = local
when :variable
variables[[current_environment.id, value]] = local
variables[[current_scope.id, value]] = local
end
end

def visit_label(node)
value = node.value.delete_suffix(":")
local = current_environment.find_local(value)
local = current_scope.find_local(value)

if local&.type == :argument
arguments[[current_environment.id, value]] = node
arguments[[current_scope.id, value]] = node
end
end
end
Expand Down Expand Up @@ -350,7 +350,7 @@ def test_double_nested_arguments
end

class Resolver < Visitor
prepend WithEnvironment
prepend WithScope

attr_reader :locals

Expand All @@ -364,10 +364,10 @@ def visit_assign(node)
level = 0
name = node.target.value.value

environment = current_environment
while !environment.locals.key?(name) && !environment.parent.nil?
scope = current_scope
while !scope.locals.key?(name) && !scope.parent.nil?
level += 1
environment = environment.parent
scope = scope.parent
end

locals << [name, level]
Expand Down