Clarify name bindings in namespaces.

Pull request

Table of contents

Abstract

  • Require namespace members be declared in the same name scope as the namespace is declared.
    • Declaring namespaces outside file scope is disallowed, so this means only at file scope.
  • Allow binding patterns to directly declare names in namespaces.
  • Disallow introducing bindings in different namespaces in the same pattern.

Problem

While the trivial case of class NS.C seems to be supported as a consequence of proposal #107: Code and name organization, it lacks detail. For example, there are a couple syntactic options when binding multiple names.

Also, there’s no clear decision around code such as:

namespace NS;
class ClassT {
  // Is this a class member accessed through `NS`, or a file scope member inside
  // `NS`? What is its lifetime?
  var NS.a: i32 = 0;
}

This proposal mainly aims to remove ambiguities.

Background

Namespaces are covered in code and name organization. Binding patterns are covered in pattern matching.

There’s some discussion of var in values, variables, and pointers, but it’s specific to locals. That doesn’t address other use cases, such as globals or member variables.

Proposal

When used to declare names in binding patterns, as in var or let, all names must be in the same namespace. namespace members must be declared from within the same scope that declared the namespace.

There was uncertainty about whether namespaces could be declared outside of file scopes; for now, disallow it.

See the changes to code and name organization for reference.

Rationale

  • Code that is easy to read, understand, and write

    • Requiring that declarations of multiple names use NS.a syntax is consistent with the single variable case.
    • Requiring namespace members be declared while in the same name scope as the namespace itself makes lifetimes clearer.

Alternatives considered

Allow prefixing a tuple binding pattern with a namespace

We could use the namespace to prefix the binding tuple. For example:

var NS.(a: i32, b: i32) = (3, 4);

It’s rare that we would have a single statement declare multiple names. As a consequence, the separation of the namespace qualifier from the declared identifier might end up unique to this syntax. In that context, we prefer NS.a for consistency with other cases, such as class NS.class.

Allow binding patterns to declare names in multiple namespaces

We could allow binding patterns to declare names in multiple namespaces. For example:

namespace NS;
var (NS.a: i32, b: i32) = InitData();

Mixing namespaces could be confusing: for example, b could be misunderstood to be declared in NS. We lack data that would demonstrate benefits to offset that.

We disallow mixing namespaces in a single declaration for simplicity.

Allow declaring names in namespaces not owned by the current scope

We could allow declaring names in namespaces not owned by the current scope. For example:

namespace NS;
class ClassT {
  var NS.val: i32;

  class NS.ChildT {}
}

Here, package.NS.val would be a global, but ClassT.NS.val looks more like an instance member. It’s also unclear whether ClassT.NS.val (or instance.NS.val) could be used to reference the produced variable, since NS is not inside ClassT’s name scope. The naming problems extend to non-binding declarations such as NS.ChildT, too.

Disallowing using namespaces to cross name scopes is consistent with rules that generally prevent declaring names in other name scopes, such as:

class A {
    class B {
        // `C` must be declared directly inside `A`.
        class A.C;
    }
}

// `D` must be declared within `A`, even if separately defined.
class A.D {}

Both the namespace declaration and names declared within the namespace must be written in the same name scope. This avoids name lookup ambiguities, and builds consistency in name scope boundaries across declarations.

Allow declaring namespaces in scopes other than the file scope

We could allow declaring namespaces in scopes other than the file scope. It’s ambiguous what path should have resulted from proposal #107: Code and name organization, although examples all focus on file scope, so other scopes weren’t carefully considered.

It might prove useful in some situations. For example, perhaps a complex class would find a member namespace useful:

class Complex {
   namespace OptionSet1;
   class OptionSet1.MemberClassA;
   class OptionSet1.MemberClassB;

   namespace OptionSet2;
   class OptionSet2.MemberClassC;
   class OptionSet2.MemberClassD;

   namespace Vars;
   var Vars.a;
}

This proposal takes a stance against declaring namespaces other than the file scope because:

  • Proposal #107: Code and name organization only mentions file scope namespaces, implicitly disallowing it in other scopes.
  • Disallowing namespaces in other scopes is consistent with C++.

If allowed, it would be necessary to decide whether Complex.Vars.a would have instance or global lifetime.

For now, namespaces may only be declared at file scope, which gives consistency with C++. This decision may be reevaluated in a future proposal.