Namespacing support Last updated Feb 11, 2025

Terminology

NS resolver module - a module containing module aliasing equations; “ns-resolver” for short.

NS library - collection of modules aliased by an ns-resolver module. Not necessarily archived or even aggregated as ocaml_library.

NS - namespace determined by the name of an ns-resolver. Note that every module name determines a namespace; OBazl uses “ns” and “namespace” specifically to refer to namespaces determined by ns-resolver modules.

module-path - Segmented module name like A.B.C; defined by the language. Has no relation to file system directory paths.

ns-submodule - a module in an ns-lib, accessible using the OCaml module path formed from the ns name, e.g. Foo.A is an ns-submodule in ns Foo.

ns-qualified module name: name with ns prefix. ns-qualified names emulate filesystem-based namespacing. For example, foo__A.ml emulates foo/a.ml.

resolved ns-submodule name - the RHS of an aliasing equation. E.g. given module A = Foo__A, the resolved ns name of ns-submodule Foo.A is Foo__A (ns-qualified). Given module A = B, the resolved ns name is B (unqualified)

An ns-lib may contain ns-submodules that are not ns-qualified.

Defining ns libraries: ocaml_ns

The ocaml_ns rule defines a namespace.

ocaml_ns(
    name = "nsFoo",
    ns   = "foo",
    submodules = [":A", ...],
    ...
)
  • This defines a filesystem prefix by suffixing __ to the value of the ns attribute. For example, if ns = "foo", then the prefix will be foo__. The filesystem prefix will be used to construct the filenames (and thus module names) of the ns-submodules listed in the submodules list.

  • It generates a file containing module aliasing equations for the modules listed in the submodules attribute. In this example, module A = Foo__A. The module prefix Foo__ is formed by capitalizing the first character of the filesystem prefix.

  • It passes the filesystem prefix on to clients in its provider (OCamlNsResolverProvider) as fs_prefix. Clients use this to construct namespaced filenames.

Modules register their membership in the namespace by using it in the ns attribute. For example:

ocaml_module(
    name          = "A",
    ns            = ":nsFoo",        (1)
    struct        = "a.ml",
    ...
)
1 Here :nsFoo is the label of the ocaml_ns build target; it is not the namespace name.

In this example, the ocaml_ns build target generates an ns-resolver module and passes its ns_prefix (foo__) to this client target , so it will compile a.ml to foo__A.cmi and foo__A.cma.

Clients that want to use the namespace can list it in either the deps or the open attribute.

Composing ns libraries

OCaml has several methods for “importing” modules:

  • include M - imports and exports all definitions in module M

  • open M - imports but does not export all definitions in module M

OBazl namespace libraries support analogous methods for importing modules, which are expressed by attributes on the ocaml_ns rule:

submodules

Type: string_list

“Endogenous” ns-submodules are modules that elect membership in the ns by using the ns attribute of the ocaml_signature and ocaml_module rules. Endogenous ns-submodules are namespace-prefixed; for example, if A is an endogenous ns-submodule of ns Foo, then a.ml will be compile to foo_A.cmi etc.

Endogenous ns-submodules must be listed in the submodules attribute. They must be listed by (unprefixed) name, as strings not labels.

ocaml_ns(name = "nsRGB", ns = "RGB", submodules = ["Red", "Green", "Blue"])

The ns-submodules must use the ns attribute to elect membership in the ns:

ocaml_module(name = "Red",   ns = ":nsRGB", struct = "red.ml")
ocaml_module(name = "Green", ns = ":nsRGB", struct = "green.ml")
ocaml_module(name = "Blue",  ns = ":nsRGB", struct = "blue.ml")

The generated ns-resolver module (nsRGB.ml) will contain:

module Red = RGB__Red
module Green = RGB__Green
module Blue = RGB__Blue

import_as

Type: label_keyed_string_dict

“Exoogenous” ns-submodules are modules that are included in an ns-library, but they do not elect membership and so are not renamed using the ns-prefix of the namespace. But they may be aliased within the namespace library.

For example, suppose the targets listed here in the import_as dictionary are plain ocaml_module targets, with no namespace election.

ocaml_ns(
    name = "nsColors", ns = "Colors",
    import_as = {
        "//ns/bottomup/import_as/colors/rgb:Red"   : "R",
        "//ns/bottomup/import_as/colors/rgb:Green" : "G",
        "//ns/bottomup/import_as/colors/rgb:Blue"  : "B",
    }
)

The generated ns-resolver module (Colors.ml) will contain:

module R = Red
module G = Green
module B = Blue

This makes the included modules accessible as Colors.R, Colors.G, and Colors.B. The OBazl dependency management logic will ensure that the resolved modules - the original Red.cmi etc. - are available where needed.

Note that import_as modules can be located anywhere in your project.

ns_import_as

Type: label_keyed_string_dict

With the ns_import_as attribute you can include other namespaces as components of your namespace under new names.

For example, suppose

//ns/bottomup/embed/colors/rgb:nsRGB

is a namespace whose name is Rgb, with ns-submodules Rgb.Red, Rgb.Green, and Rgb.Blue. You can make this available within namespace Foo under the name Bar like so:

ocaml_ns(
    name = "nsFoo", ns = "foo",
    ns_import_as = {"//ns/bottomup/embed/colors/rgb:nsRGB": "Bar"}
)

This aliases Foo.Bar.Red to Rgb.Red etc.

The implementation is simple. The ns_import_as attribute has type label_keyed_string_dict, which means the keys are labels and the values are strings. Since the keys are labels of ocaml_ns targets, the rule implementation can extract the original namespace name, and use it and the string values to construct aliases, e.g.

module RGB = Rgb

And since the keys are dependencies, the rule implementation can ensure that they are listed as inputs to any compile/link actions.

ns_merge

Type: label_list

Merges one namespace into another, making its submodules accessible directly.

In this example, suppose //ns/bottomup/ns_merge/colors/rgb:nsRGB is an ocaml_ns target, defining a namespace whose name is RGB, with ns-submodules RGB.Red, RGB.Green, and RGB.Blue. Using ns_merge to import this ns is analogous to using OCaml’s include to import a module and export its contents.

ocaml_ns(
    name = "nsColors", ns = "Colors",
    ns_merge = ["//ns/bottomup/ns_merge/colors/rgb:nsRGB"]
)

Because the original ns-resolver module RGB.ml contains

module Red = RGB__Red
module Green = RGB__Green
module Blue = RGB__Blue

the generated ns-resolver module (Colors.ml) will contain:

module Red = RGB__Red
module Green = RGB__Green
module Blue = RGB__Blue

This aliases Foo.Red to the same module as RGB.Red does, etc.

Quasi-private ns libraries

Bazel’s visibility attribute controls visibility of build targets. Targets marked with visibility=["//visibility:private"] are visible and accessible only to targets within the same Bazel package.

To make a namespace (Bazel-) private, set the visibility attribute of ocaml_ns to private:

ocaml_ns(
    name = "nsRe",
    ns   = "re",
    visibility = ["//visibility:private"],
    ...
)

OBazl uses a naming convention to make namespaces quasi-private. I learned this from Dune, but it is not specific to any build system.

The namespace resolver module file generated by this target will be named re__.ml, and any ns-submodule A in this namespace will named re__A.ml . The trailing __ in the resolver module name is intended to prevent name clashes, in case the Bazel package also contains a module file with the same name as the namespace (e.g. re.ml).

If the visibility were set to anything other than private, then the resolver module file would be named re.ml (without the trailing __), but the ns-submodules in the namespace would still be renamed with the same prefix (re__).

Note that there are two concepts in play here, Bazel’s notion of visibility, and our internal use of suffix __ to prevent name clashes, so ns resolver names like foo__.ml will not clash with foo.ml. So that suffix makes the ns quasi-private, not in the sense of restricting its visibility (as Bazel’s visibility attribute does), but in the sense of requiring, of users who want to access it, that they use the awkward module name Foo__.

If re.ml depends on (the ns-submodules in) the namespace, use the open attribute:

ocaml_module(
    name   = "Re",
    struct = "re.ml",
    open   = [":nsRe"],        (1)
    ...
)
1 Since nsRe is the label of the private ocaml_ns target, which produces ns resolver module re__.ml, this will translate into command line option -open Re__.
open = [":nsRe"] is NOT to be confused with ns = ":nsRe"! The latter expresses membership in the namespace; the former indicates a dependency on the namespace, but not membership.

An alternative strategy for handling the case where you have a module with the same name as the ns is to give the namespace a completely different name (meaning the ns attribute, not the name attribute). For example:

ocaml_ns(
    name = "nsRe",
    ns   = "nsre",
    visibility = ["//visibility:public"],
    ...
)

This would generate resolver module nsre.ml, and ns-submodule prefix nsre__. The re.ml module declared above will work unchanged, since in this case open = [":nsRe"] will translate to -open Nsre.

Implementation

Implementation code is in impl_ns_resolver.bzl.

Tasks:

  • Derive ns name, ns_prefix, fs_prefix, etc.

  • Construct aliasing equations for ns-submodules

    • attr submodules - use ns_prefix

    • attr import_as - exogenous modules, not ns-prefixed

    • attr ns_import_as - exogenous ns libs, exported under their ns

    • attr open - exogenous ns libs, submods exported under local ns

The fs prefix will always have suffix __. It will only be used for submodules elements.

If visibility is set to private, then the ns name will match the fs_prefix, in having suffix __.