Modules and Sources

In this section, we'll see how to use modules in Neut.

Table of Contents

Basics of Modules

Creating, Building, and Executing a Module

Let's create a module by running the following command:

neut create sample

This command creates a template module ./sample/ that performs "hello world".

You can build and execute this module as follows:

cd ./sample
neut build sample --execute # => "Hello, world!"

You can also retrieve the resulting binary:

neut build sample --install ./bin # creates a directory `bin` if necessary
./bin/sample # => "Hello, world!"

Structure of a Module

The structure of a module is as follows:

sample/
├── cache/
│  └── ...
├── source/
│  └── sample.nt
└── module.ens

The directory cache is where object files (binary files) and dependencies are put. You don't normally have to go into the directory.

The directory source is where source files are put.

The file module.ens contains meta information about this module, such as dependencies.

You can change the locations of special directories such as cache using module.ens. See Modules for more information.

module.ens

The content of module.ens is something like the following:

{
  target {
    sample {
      main "sample.nt",
    },
  },
  dependency {
    core {
      digest "(base64url-encoded checksum)",
      mirror [
        "https://github.com/.../X-Y-Z.tar.zst",
      ],
      enable-preset true,
    },
  },
}

target specifies the targets of a module. In the example above, the command neut build sample builds the module using the file source/sample.nt as its entry point.

dependency specifies the dependencies of a module. Since our running example doesn't do much, the only dependency is core, which is the same as "prelude" in other languages.

digest is the base64url-encoded checksum of a dependency.

mirror is a list of URLs of a dependency.

enable-preset makes the dependency behave similarly to the Prelude in Haskell. That is, when enable-preset is set to true, the names specified in the dependency are automatically imported into every file in our module. This field should be set to true only for the core library.

Basics of Source Files

Editing Source Files

Let's see the content of source/sample.nt:

// sample.nt

define main(): unit {
  print("Hello, world!\n") // `print` is defined in `core`
}

The above code defines a function main that returns a value of type unit. This function prints "Hello, world!\n".

Let's try editing the code as follows:

// sample.nt

import {
  core.text.io {print-int},
}

define main(): unit {
  print-int(42) // `print-int` is also defined in `core`
}

Then, build and execute the project:

neut build sample --execute # => 42

You should be able to see that the result changes accordingly.

Defining Functions

Of course, you can define a function:

// sample.nt

import {
  core.text.io {print-int},
}

define get-int(): int {
  42
}

define main(): unit {
  print-int(get-int())
}

A function can take arguments. Let's rewrite sample.nt as follows:

// sample.nt

import {
  core.text.io {print-int},
}

define increment(x: int): int {
  add-int(x, 1)
}

define my-add(x: int, y: int): int {
  add-int(x, y)
}

define main(): unit {
  print-int(my-add(10, increment(10))) // # => 21
}

Top-level items like define are called statements. You’ll learn more about them in the next section.

As in F#, statements in Neut are order-sensitive. Therefore, if you define main before my-add, the code won't compile. For forward references, you have to explicitly declare names beforehand using a statement called nominal, which we'll see in the next section.

Publishing Modules

Let's publish our module so others can use increment and my-add.

You can create a tarball snapshot of your module using neut archive:

neut archive 0-1
ls ./archive # => 0-1.tar.zst

The argument of neut archive must be something like 0-1-0, 2-3-1, or 1-2-3-4-5-6. The compiler interprets these names as semantic versions.

You can then upload these tarballs by pushing them to GitHub, for example.

Adding Dependency Modules

neut get can be used to add dependencies:

# creates a new module
neut create new-item
cd new-item

# adds a sample module that contains `my-add` and `increment` to your module
neut get some-name https://github.com/vekatze/neut-sample/raw/main/archive/0-1.tar.zst

The command neut get fetches the tarball from the specified URL and adds it to the current module. The module can then be used as some-name in your module.

The information of the newly-added module is saved to module.ens:

{
  target {
    new-item {
      main "new-item.nt",
    },
  },
  dependency {
    core { .. },
    // ↓ HERE
    some-name {
      digest "..",
      mirror [
        "https://github.com/YOUR_NAME/YOUR_REPO_NAME/raw/main/archive/0-1.tar.zst",
      ],
    },
  },
}

The "real" name of a dependency is the digest of the tarball. Names such as some-name are just aliases.

Importing Files

Importing Files in Dependencies

Dependencies can be used in your code, of course:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample {my-add}, // imports `my-add` in `source/sample.nt`
}

define main(): unit {
  print-int(my-add(10, 11)) // ← using `my-add`
}

Let's focus on import. import consists of lines like the one below:

some-name.sample {my-add}

The first component of such a line (some-name) is our alias of the dependency.

What follows (sample) is the relative path to the file from the source directory of the dependency module. Here, you don't have to write the file extension .nt.

Like {my-add} in the example above, every item of an import can optionally have a list of names. Names in these lists are made available after import.

You can also use the fully-qualified form of my-add:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample, // removed `{my-add}`
}

define main(): unit {
  // ↓ using the fully-qualified form of `my-add`
  print-int(some-name.sample.my-add(10, 11))
}

Also, files like source/foo/item.nt in some-name can be imported as follows:

import {
  some-name.foo.item,
}

Importing Files in the Current Module

Let's try creating a new file new-item/source/foo/greet.nt with the following content:

// foo/greet.nt

define yo(): unit {
  print("Yo")
}

This file can then be used from new-item/source/new-item.nt as follows:

// new-item.nt

import {
  this.foo.greet {yo},
}

define main(): unit {
  yo()
}

That is, the name of the current module is always this.

Defining Aliases for Files

Aliases for files can also be defined. Let's look again at the example of fully-qualified names:

// new-item.nt

import {
  core.text.io {print-int},
  some-name.sample,
}

define main(): unit {
  print-int(some-name.sample.my-add(10, 11))
}

Let's define an alias for some-name.sample. Edit the module.ens as follows:

{
  target {..},
  prefix {                  //
    S "some-name.sample",   // ← alias: S == some-name.sample
  },                        //
  dependency {..},
}

The new-item.nt can then be rewritten as follows:

// new-item.nt

import {
  core.text.io {print-int},
  S, // == some-name.sample
}

define main(): unit {
  print-int(S.my-add(10, 11)) // S.my-add == some-name.sample.my-add
}

Unlike Haskell, these prefixes are defined per module, not per file. The prefixes of a module must be consistent throughout the module.