ocaml_ns( name = "nsFoo", ns = "foo", submodules = [":A", ...], ... )
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.
-
This defines a filesystem prefix by suffixing
__
to the value of thens
attribute. For example, ifns = "foo"
, then the prefix will befoo__
. The filesystem prefix will be used to construct the filenames (and thus module names) of the ns-submodules listed in thesubmodules
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 prefixFoo__
is formed by capitalizing the first character of the filesystem prefix. -
It passes the filesystem prefix on to clients in its provider (
OCamlNsResolverProvider
) asfs_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 __
.