Exotic filenames (think Example.test.res) and namespacing

I was writing tests for my code and ran into some strange compiler errors. In the end, a simple file rename took all my problems away, but after the fact I decided to take a closer look. Investigation results follow. I think I’m onto something. :slight_smile:

I’m a bit unsure if this is something that would need fixing or not, and whether the fix would be an addition to documentation, better error messages, or a change somewhere in the compiler.

Setup

Consider a project structure like the following:

example
|---bsconfig.json
|---package.json
\---src
    |---Command.res
    |---Example.res
    \---Library.res

Example.res and Library.res are stand-alone, library type files, while Command.res uses types and values from the other modules. Code works. So far so good!

As a minimal example the Example.res and Library.res files could contain the following:

type t = string
let a: t = "example string"

And Command.res could contain the following:

let exampleValue = Example.a
type exampleType = Example.t
let libraryValue = Library.a
type libraryType = Library.t

Now, imagine the author wants to write some unit tests documenting module usage. One common naming convention would call for a file called Example.test.res. This is an exotic module filename and the documentation warns that it would not be accessible from other modules. Sounds like an acceptable tradeoff from sticking with a familiar naming scheme, doesn’t it?

How it works out depends on bsconfig.json values.

Scenario 1

  • namespace is false

Result:

  • Example.test.res cannot access values or types from Example.res: The module or file Example can’t be found.
  • Example.test.res can access Library.res and Command.res.

Demo:

Example.test.res
/* Scenario 1 - namespace: false */
/* let exampleValue = Example.a // The module or file Example can't be found. */
/* type exampleType = Example.t // The module or file Example can't be found. */
/* let libraryValue = Library.a // OK */
/* type libraryType = Library.t // OK */
/* let commandExampleValue = Command.exampleValue // OK */
/* type commandExampleType = Command.exampleType // OK */
/* let commandLibraryValue = Command.libraryValue // OK */
/* type commandLibraryType = Command.libraryType // OK */

Scenario 2

  • namespace is true
  • name is my-example (or something else but not example – and not matching any other ReScript modules just to be extra sure)

Result:

  • Everything works as expected.

Demo:

Example.test.res
/* Scenario 2 - namespace: true, name: my-example */
/* let exampleValue = Example.a // OK */
/* type exampleType = Example.t // OK */
/* let libraryValue = Library.a // OK */
/* type libraryType = Library.t // OK */
/* let commandExampleValue = Command.exampleValue // OK */
/* type commandExampleType = Command.exampleType // OK */
/* let commandLibraryValue = Command.libraryValue // OK */
/* type commandLibraryType = Command.libraryType // OK */

Scenario 3

  • namespace is true
  • name is example

Result:

  • Example.test.res cannot access values from any of the other modules: Internal path Example.XXX is dangling.
  • Example.test.res cannot access types from any of the other modules: Fatal error: exception Not_found

Demo:

Example.test.res
/* Scenario 3 - namespace: true, name: example */
/* let exampleValue = Example.a // Internal path Example.Example is dangling. */
/* type libraryType = Library.t // Fatal error: exception Not_found */
/* let libraryValue = Library.a // Internal path Example.Library is dangling. */
/* type exampleType = Example.t // Fatal error: exception Not_found */
/* let commandExampleValue = Command.exampleValue // Internal path Example.Command is dangling. */
/* type commandExampleType = Command.exampleType // Fatal error: exception Not_found */
/* let commandLibraryValue = Command.libraryValue // Internal path Example.Command is dangling */
/* type commandLibraryType = Command.libraryType // Fatal error: exception Not_found */

Questions and discussion

First and foremost, shouldn’t namespace settings be completely irrelevant to any of this? The build configuration documentation says in bold letters: “The namespacing affects your consumers, not yourself.” I think I’ve found a case where namespacing most definitely affects me.

Should a module with an exotic filename (e.g., one with .test. affix) be able to access a module without the extra part in the middle? If this is not a supported use case, does it work by mere luck in scenario 2? Could there be a clear warning against such use here: https://rescript-lang.org/docs/manual/latest/module#exotic-module-filenames? For writing tests, the documentation might recommend using a non-exotic filename such as ExampleTest.res instead. That kind of small documentation change would be a very low-hanging fruit.

I would have expected the module with the exotic filename to be able to access other, unrelated modules in scenario 3 even if the non-exotic module with the same base name was acting up. And… what exactly is wrong here? The error messages are hard to understand. What on earth is a “dangling path” and can I, as a user, do something about it? Also, why does accessing a type result in a different error that looks like quite a hard crash? If this is something that is not supported or otherwise OK to fail, a more detailed error message would make for a better developer experience.

Also comes to mind, is scenario 3 a rare corner case related to exotic file names only, or could the combination of matching namespace and module names be problematic in other ways, too?

To wrap it up, my acute problem was easy to work around by simply changing the name of the file, and other workarounds exist. Still, there seems to be a growing interest in exotic module filenames, and I think having things like this as polished as possible is gaining relevance.

3 Likes