Lily's predefined symbols are all available in the prelude module. It is the only module that has all symbols available at all times, and which cannot be imported. All other modules operate in a scope by themselves. Symbols from one module are not available to other modules, unless imported. Import can either name specific symbols, or opt to use the module as a namespace.

Lily allows modules to have code outside of a definition. If toplevel code exists, it is run the first the module is imported.

# file: fibmodule.lily
define fib(n: Integer): Integer
{
    if n < 2: {
        return n
    else:
        return fib(n - 1) + fib(n - 2)
    }
}

# file: other.lily
import fibmodule

# Invalid because fib is not in this scope.
# print(fib(10))

# Invalid because fibmodule is not a value.
# print(fibmodule)

define fib(n: Integer): Integer
{
    return fibmodule.fib(n)
}

print(fib(10)) # 55

By importing fibmodule, other is able to access all symbols inside of fibmodule by using fibmodule as a namespace.

Mechanics

Lily's import may look like Python's at first glance, but there are some important distinctions.

The Lily interpreter is designed to be embedded in other programs. Embedders are able to register a module for use by scripts they run. One example of that is FascinatedBox/lily-apache, which makes a server module available to scripts it runs.

Another important distinction is the dynaload mechanism in the interpreter. When a foreign module such as sys is imported, the interpreter does not automatically load all symbols inside. Instead, it transparently loads symbols as they are referenced in the source module. If import sys is run, sys.argv is not loaded until sys.argv is seen. As a result, the interpreter does not export modules as values.

The most important different is how Lily selects paths when attempting to do importing.

This example farm program should help to illustrate why Lily's import mechanism works the way it does.

In the beginning, there is only the farm.

    farm.lily

This farm is empty. It needs a barn for storage, and workers.

    barn.lily
    farm.lily
    farmer.lily

Right now, farm can import farmer and import barn, and all works well. But these files need to be contained somewhere. After some reorganization, this emerges.

    farm/
        src/
            barn.lily
            farm.lily
            farmer.lily

What's a farm without a field of items to grow?

    farm/
        src/
            farm.lily
            farmer.lily
            barn.lily

            field/
                beans.lily
                brussel sprouts.lily
                cauliflower.lily
                peas.lily

Might as well place workers in their own space too.

    farm/
        src/
            barn.lily
            farm.lily

            field/
                beans.lily
                brussel sprouts.lily
                cauliflower.lily
                peas.lily

            worker/
                farmer.lily

Suppose the farmer wants to import food from the field to use it. How should the farmer refer to it?

Running the farm starts from farm.lily and spreads out from there. Lily solves this question by saying that the first file loaded is a root module. When loading modules, paths need to be specified relative to a root module.

# file: farmer.lily

# This becomes "beans".
import "field/beans"

# By default, this does not get an identifier.
# A name can be given as the identifier, but is not required.
import "field/brussel sprouts" as sprouts

# file: beans.lily

import "field/peas"

Even though beans.lily is in the same directory as peas.lily, it must still pass field/peas instead of only peas to import.

Farms often have a tractor that's made somewhere else. Assume files for a tractor were copied in as follows.

    farm/
        src/
            barn.lily
            farm.lily

            field/
                beans.lily
                brussel sprouts.lily
                cauliflower.lily
                peas.lily

            tractor/
                base_tractor.lily
                tractor.lily
                wheel.lily

            worker/
                farmer.lily

This does not work!

Because the tractor's files were copied in, they assume that the tractor is the root module. Since barn.lily is the root, they no longer work.

Similarly, if the files from farm were copied into another project, they would fail for the same reason.

Note how farm begins at farm/src/farm.lily. Suppose tractor could do the same. A near-final version of the farm looks like this.

    farm/
        src/
            barn.lily
            farm.lily

            field/
                beans.lily
                brussel sprouts.lily
                cauliflower.lily
                peas.lily

            packages/
                tractor/
                    src/
                        tractor.lily

            worker/
                farmer.lily

When the farmhand farmer.lily asks to import tractor, Lily uses the second strategy of looking at packages/<name>/src/<name> relative to farm.lily.

When Lily imports a module through a packages directory, it marks the module it sees as a root module. Modules inside tractor will be relative to tractor, and farm relative to farm.

What if the tractor does not want to export all details to the farm? A final example is as follows.

    farm/
        src/
            barn.lily
            farm.lily

            field/
                beans.lily
                brussel sprouts.lily
                cauliflower.lily
                peas.lily

            packages/
                tractor/
                    src/
                        base_tractor.lily
                        tractor.lily
                        run_tractor.lily
                        wheel.lily

            worker/
                farmer.lily

base_tractor.lily defines all the symbols that the tractor needs. It imports wheel through import wheel, as well as any other files it may need.

tractor.lily imports the parts of base_tractor.lily that are meant to be visible to the public.

run_tractor.lily is a file to execute to run the tractor directly.

wheel.lily is a file imported by base_tractor.lily.

Because of how Lily's importing works, the tractor package can assume that only tractor.lily will be imported by the outside.

In short, importing in Lily is relative to a root module. Given an import of farm and search for barn, it will search as follows.

barn.lily
barn.{dll/dylib/so}
packages/barn/src/barn.lily
packages/barn/src/barn.{dll/dylib/so}

Features

Redeclaration

By default, a module's name is the filename minus the suffix. It is possible to give the interpreter a different name to use instead.

This cannot be mixed with direct imports (import (<symbols>) <target>).

# file: fibmodule.lily
define fib(n: Integer): Integer
{
    if n < 2: {
        return n
    else:
        return fib(n - 1) + fib(n - 2)
    }
}

# file: other.lily
import fibmodule as newfib

print(newfib.fib(10)) # 55

Multi

It is also possible to import multiple modules at once. This example imports multiple predefined modules. Each entry in a multi import can execute any import features mentioned here.

# file: multi.lily
import introspect, sys, time

print(time.Time.now()) # <Time at 0x--->

Direct

It is also possible to import symbols directly from other modules. Any kind of symbol except a module can be directly imported from a module.

This cannot be mixed with redeclaration (import <target> as <name>).

Symbols in the prelude module are available to all modules without importing. For all other modules, the symbol must be imported, or the module imported and used as a namespace.

For one module to access symbols from another module, it has to import the symbols directly (import (<symbol>) module), or import the module and access the symbols with the module as a namespace (import module).

# file: direct.lily
import (argv) sys,
       (Time) time

print(Time.now()) # <Time at 0x--->
print(argv)       # []

It is intentionally not possible to import all symbols from a module. By requiring all symbols to be explicitly named, it is easier to know where all symbols in a file originate from.

Path

Lily imports always use the forward slash (/) as a directory separator. On Windows, the forward slash is translated to a backslash. It is a syntax error to use a backslash.

If a path is given that has slashes, the default module name will be the filename. In the below example, somedir/point becomes point.

# file: somedir/point.lily

class Point(public var @x: Integer, public var @y: Integer)
{
    public define stringified: String {
        return "Point({0}, {1})".format(@x, @y)
    }
}

# file: first.lily

import "somedir/point"

print(point.Point(10, 20).stringified()) # Point(10, 20)