Skip to content

Commit

Permalink
Add file-private types. Part of #2950
Browse files Browse the repository at this point in the history
  • Loading branch information
Ary Borenszweig committed Sep 8, 2016
1 parent fbb5b16 commit 3a5c86e
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "../../spec_helper"
require "tempfile"

describe "Codegen: private def" do
describe "Codegen: private" do
it "codegens private def in same file" do
compiler = Compiler.new
sources = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "../../spec_helper"

describe "Semantic: private def" do
describe "Semantic: private" do
it "doesn't find private def in another file" do
expect_raises Crystal::TypeException, "undefined local variable or method 'foo'" do
compiler = Compiler.new
Expand Down Expand Up @@ -136,4 +136,101 @@ describe "Semantic: private def" do
{% end %}
)) { int32 }
end

it "doesn't find private class in another file" do
expect_raises Crystal::TypeException, "undefined constant Foo" do
compiler = Compiler.new
sources = [
Compiler::Source.new("foo.cr", %(
private class Foo
end
)),
Compiler::Source.new("bar.cr", %(
Foo
)),
]
compiler.no_codegen = true
compiler.prelude = "empty"
compiler.compile sources, "output"
end
end

it "finds private type in same file" do
compiler = Compiler.new
sources = [
Compiler::Source.new("foo.cr", %(
private class Foo
def foo
1
end
end
Foo.new.foo
)),
]
compiler.no_codegen = true
compiler.prelude = "empty"
compiler.compile sources, "output"
end

it "can use types in private type" do
assert_type(%(
private class Foo
def initialize(@x : Int32)
end
def foo
@x + 20
end
end
Foo.new(10).foo
)) { int32 }
end

it "can use class var initializer in private type" do
assert_type(%(
private class Foo
@@x = 1
def self.x
@@x
end
end
Foo.x
), inject_primitives: false) { int32 }
end

it "can use instance var initializer in private type" do
assert_type(%(
private class Foo
@x = 1
def x
@x
end
end
Foo.new.x
), inject_primitives: false) { int32 }
end

it "finds private class in macro expansion" do
assert_type(%(
private class Foo
@x = 1
def x
@x
end
end
macro foo
Foo.new.x
end
foo
), inject_primitives: false) { int32 }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ module Crystal
when TypeDeclaration
node.var.is_a?(ClassVar)
when FileNode, Expressions, ClassDef, ModuleDef, EnumDef, Alias, Include, Extend, LibDef, Def, Macro, Call, Require,
MacroExpression, MacroIf, MacroFor
MacroExpression, MacroIf, MacroFor, VisibilityModifier
true
else
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Crystal::InstanceVarsInitializerVisitor < Crystal::SemanticVisitor
when TypeDeclaration
node.var.is_a?(InstanceVar)
when FileNode, Expressions, ClassDef, ModuleDef, Alias, Include, Extend, LibDef, Def, Macro, Call, Require,
MacroExpression, MacroIf, MacroFor
MacroExpression, MacroIf, MacroFor, VisibilityModifier
true
else
false
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/crystal/semantic/new.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module Crystal
# We also need to define empty `new` methods for types
# that don't have any `initialize` methods.
define_default_new(self)
file_modules.each_value do |file_module|
define_default_new(file_module)
end
end

def define_default_new(type)
Expand Down
38 changes: 26 additions & 12 deletions src/compiler/crystal/semantic/path_lookup.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ module Crystal
#
# If the path is global (for example ::Foo::Bar), the search starts at
# the top level.
def lookup_path(path : Path, lookup_in_namespace = true) : Type | ASTNode | Nil
(path.global? ? program : self).lookup_path(path.names, lookup_in_namespace: lookup_in_namespace)
def lookup_path(path : Path, lookup_in_namespace = true, location = path.location) : Type | ASTNode | Nil
location = nil if path.global?
(path.global? ? program : self).lookup_path(path.names, lookup_in_namespace: lookup_in_namespace, location: location)
end

# ditto
def lookup_path(names : Array(String), lookup_in_namespace = true) : Type | ASTNode | Nil
def lookup_path(names : Array(String), lookup_in_namespace = true, location = nil) : Type | ASTNode | Nil
type = self
names.each_with_index do |name, i|
# The search must continue in the namespace only for the first path
# item: for subsequent path items only the parents must be looked up
type = type.lookup_path_item(name, lookup_in_namespace: lookup_in_namespace && i == 0)
type = type.lookup_path_item(name, lookup_in_namespace: lookup_in_namespace && i == 0, location: location)
return unless type

# Stop if this is the last name
Expand All @@ -58,28 +59,41 @@ module Crystal
# type's namespace. This parameter is useful because when writing
# `Foo::Bar::Baz`, `Foo` should be searched in enclosing namespaces,
# but `Bar` and `Baz` not.
def lookup_path_item(name : String, lookup_in_namespace) : Type | ASTNode | Nil
def lookup_path_item(name : String, lookup_in_namespace, location) : Type | ASTNode | Nil
# First search in our types
type = types?.try &.[name]?
return type if type

# Then try out parents, but don't search in our parents namespace
parents.try &.each do |parent|
match = parent.lookup_path_item(name, lookup_in_namespace: false)
match = parent.lookup_path_item(name, lookup_in_namespace: false, location: location)
return match if match
end

# Try our namespace, unless we are the top-level
if lookup_in_namespace && self != program
return namespace.lookup_path_item(name, lookup_in_namespace)
return namespace.lookup_path_item(name, lookup_in_namespace, location)
end

nil
end
end

class Program
def lookup_path_item(name : String, lookup_in_namespace, location)
# Check if there's a private type in location
if location && (original_filename = location.original_filename) &&
(file_module = file_module?(original_filename)) &&
(item = file_module.types[name]?)
return item
end

super
end
end

module GenericType
def lookup_path_item(name : String, lookup_in_namespace)
def lookup_path_item(name : String, lookup_in_namespace, location)
# If we are Foo(T) and somebody looks up the type T, we return `nil` because we don't
# know what type T is, and we don't want to continue search in the namespace
if type_vars.includes?(name)
Expand All @@ -90,7 +104,7 @@ module Crystal
end

class GenericInstanceType
def lookup_path_item(name : String, lookup_in_namespace)
def lookup_path_item(name : String, lookup_in_namespace, location)
# Check if *name* is a type variable
if type_var = type_vars[name]?
if type_var.is_a?(Var)
Expand All @@ -99,19 +113,19 @@ module Crystal
type_var
end
else
generic_type.lookup_path_item(name, lookup_in_namespace)
generic_type.lookup_path_item(name, lookup_in_namespace, location)
end
end
end

class UnionType
def lookup_path_item(name : String, lookup_in_namespace)
def lookup_path_item(name : String, lookup_in_namespace, location)
# Union type does not currently inherit GenericClassInstanceType,
# so we check if *name* is the only type variable of Union(*T)
if name == "T"
return program.tuple_of(union_types)
end
program.lookup_path_item(name, lookup_in_namespace)
program.lookup_path_item(name, lookup_in_namespace, location)
end
end

Expand Down
31 changes: 24 additions & 7 deletions src/compiler/crystal/semantic/top_level_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
def visit(node : ClassDef)
check_outside_exp node, "declare class"

scope, name = lookup_type_def_name(node.name)
scope, name = lookup_type_def_name(node)

type = scope.types[name]?

Expand Down Expand Up @@ -136,7 +136,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
end
end

scope.types[name] = type if created_new_type
scope.types[name] = type
node.resolved_type = type

attach_doc type, node
Expand All @@ -156,7 +156,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
def visit(node : ModuleDef)
check_outside_exp node, "declare module"

scope, name = lookup_type_def_name(node.name)
scope, name = lookup_type_def_name(node)

type = scope.types[name]?
if type
Expand Down Expand Up @@ -405,7 +405,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
attributes = check_valid_attributes node, ValidEnumDefAttributes, "enum"
attributes_doc = attributes_doc()

scope, name = lookup_type_def_name(node.name)
scope, name = lookup_type_def_name(node)

enum_type = scope.types[name]?
if enum_type
Expand Down Expand Up @@ -582,8 +582,19 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
node.exp.visibility = node.modifier
node.exp.accept self

# Can only apply visibility modifier to def, macro or a macro call
# Can only apply visibility modifier to def, type, macro or a macro call
case exp = node.exp
when ClassDef, ModuleDef, EnumDef
# Only for top-level types
if current_type.is_a?(Program)
if node.modifier.private?
return false
else
node.raise "can only use 'private' for file-level types"
end
else
node.raise "can only apply visibility modifier to file-level types, not #{current_type}"
end
when Def
return false
when Macro
Expand Down Expand Up @@ -812,7 +823,13 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
end
end

def lookup_type_def_name(path)
def lookup_type_def_name(node : ASTNode)
scope, name = lookup_type_def_name(node.name)
scope = program.check_private(node) || scope
{scope, name}
end

def lookup_type_def_name(path : Path)
if path.names.size == 1 && !path.global?
scope = current_type
name = path.names.first
Expand All @@ -831,7 +848,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
unless target_type
next_type = base_type
path.names.each do |name|
next_type = base_type.lookup_path_item(name, lookup_in_namespace: false)
next_type = base_type.lookup_path_item(name, lookup_in_namespace: false, location: path.location)
if next_type
if next_type.is_a?(ASTNode)
path.raise "execpted #{name} to be a type"
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/type_lookup.cr
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class Crystal::Type
if node.names.size == 1
return free_var
elsif free_var.is_a?(Type)
type = free_var.lookup_path(node.names[1..-1], lookup_in_namespace: false)
type = free_var.lookup_path(node.names[1..-1], lookup_in_namespace: false, location: node.location)
end
else
type = @root.lookup_path(node)
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@ module Crystal
property splat_index : Int32?
property? abstract : Bool
property? struct : Bool
property visibility = Visibility::Public

def initialize(@name, body = nil, @superclass = nil, @type_vars = nil, @abstract = false, @struct = false, @name_column_number = 0, @splat_index = nil)
@body = Expressions.from body
Expand Down Expand Up @@ -1250,6 +1251,7 @@ module Crystal
property splat_index : Int32?
property name_column_number : Int32
property doc : String?
property visibility = Visibility::Public

def initialize(@name, body = nil, @type_vars = nil, @name_column_number = 0, @splat_index = nil)
@body = Expressions.from body
Expand Down Expand Up @@ -1719,6 +1721,7 @@ module Crystal
property members : Array(ASTNode)
property base_type : ASTNode?
property doc : String?
property visibility = Visibility::Public

def initialize(@name, @members = [] of ASTNode, @base_type = nil)
end
Expand Down

0 comments on commit 3a5c86e

Please sign in to comment.