icerpc / slicec

The Slice compiler library
Apache License 2.0
13 stars 5 forks source link

Add Support for 'Using' directives #74

Open InsertCreativityHere opened 2 years ago

InsertCreativityHere commented 2 years ago

Many languages provide a way of 'pulling in' other namespaces:

Slice should also provide a feature like this, to avoid needing to repeat long namespaces.

Proposed Syntax

using <qualified_module>;

using IceRpc::Internal;
// Now anything in `IceRpc::Internal` can be accessed without qualification

It MUST be declared at the top of a slice file, before any module declarations (Same as file attributes). Additionally, it's file-specific and NOT exported. Only the contents of the slice file will be affected by this.

Open Questions What happens when there's a conflict? If a locally declared type conflicts with a type that is being pulled in. A) We silently hide the pulled-in type, and the locally declared type is what gets used B) We issue an error saying that the type is ambiguous. I lean heavily towards B, otherwise it seems very unsafe to me. But apparently C# went with option A, so idk.

pepone commented 2 years ago

I think C# behavior is quite good, the locally defined type hides imported types, there is only a conflict if you use a type not defined locally that is defined in more that one of the imported namespaces.

Unlike a using_alias_directive, a using_namespace_directive may import types whose identifiers are already defined within the enclosing compilation unit or namespace body. In effect, names imported by a using_namespace_directive are hidden by similarly named members in the enclosing compilation unit or namespace body.

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/namespaces#using-namespace-directives

Think of something like

// Foo.slice
module Foo
{

    interface Bar{}
    interface Baz{}
}

// Foe.slice
using Foo;

module Foe
{
    interface Bar // Doesn't make sense for this to be an error
    {
        Baz getBaz(); // Baz refers to Foo::Baz
    }
}

This seems also consistent with what we do for nested modules

module Foo
{
    interface Bar {}

    module Nested
    {
        interface Bar {} // Hides Foo::Bar
    }
}
InsertCreativityHere commented 2 years ago

This seems also consistent with what we do for nested modules

Yes, you're right. I didn't think about that.

So, type resolution would look like: 1) Check the current module for the type 2) Recursively check enclosing modules for the type, working outwards (from current module to global scope) 3) Check all the modules you're using for the type. If only 1 module has the type, use it, if multiple modules have it, report an ambiguity error

Importantly, we wouldn't check the enclosing modules for using directives. Either the type is directly in that module or it isn't.

externl commented 2 years ago

I prefer that we error on any ambiguity and make the user qualify it.


// A.slice
module A 
{
    interface Foo {}
}

/// B.slice
using A;
module B 
{
    interface Foo // not ambiguous  
    {
        getAFoo() ->  A::Foo; // not ambiguous  
        getBFoo() ->  B::Foo; // not ambiguous  
        getFoo() -> Foo; // ambiguous (fails to compile)
    }

    module C 
    { 
        interface Foo {} // not ambiguous  

        struct Data
        {
            f1: C::Foo // not ambiguous
            f2: A::Foo // not ambiguous
            f3 B::Foo // not ambiguous
            f: Foo // ambiguous
        }
    }
}
pepone commented 2 years ago

I find it odd that you will have to use the scope of the current module, and it is not consistent with nested module rules

module Foo
{
    interafece A
    {
        getA() -> A // not ambiguous Foo::A
    }

    module Bar
    {
        interafece A
        {
            getA() -> A // not ambiguous Foo::Bar::A
        }
    }
}

Also consider you can be adding using A because you are trying to bring a few types in the current context, but why bother having to qualify all the types event the ones you are not trying to use. Also another case is

// A.slice
module A 
{
    interface Bar{}
}

/// B.slice
using A;
module B 
{
    interface Foo // not ambiguous 
    {
        getBar() ->  Bar; // not ambiguous  
        getFoo() ->  Foo; // not ambiguous  
    }
}

Now you go and add interface Foo to A.slice and this breaks compilation of B.slice even when B.slice doesn't care about this new type?

externl commented 2 years ago

I find it odd that you will have to use the scope of the current module, and it is not consistent with nested module rules

I updated my example, that's also ambiguous.

Now you go and add interface Foo to A.slice and this breaks compilation of B.slice even when B.slice doesn't care about this new type?

Isn't that just a problem in general with "including" things from different files.