Threadon / Imports

Imports

splitting code across files

What a module is

A module is just another .th file. The file that starts the program is the main file; any file it imports is a module. Modules export their functions and structs; everything else stays private.

There are four import forms: import, from…import, lazyimport, and lazyfrom.

import: the whole module

import makes the module's functions and structs available under the module's name. Call them qualified, with a dot.

import math

def main() -> Int32
    print(math.abs(-5))
    return 0

An alias renames the module in your file:

import math as m
print(m.abs(-5))

from: pick names

from pulls specific members in by their plain name, with optional aliases. Only functions and structs can be imported.

from math import abs, clamp
from math import pow as power

Both import styles can be mixed in one file, as in the 03_imports example:

from math import abs, clamp
import math

def main() -> Int32
    a: Int32 = abs(-5)
    b: Int32 = math.clamp(99, 0, 50)
    c: Int32 = math.abs(-9)
    print(a + b + c)
    return 0
64

Lazy imports

lazyimport and lazyfrom are the same thing, but the module isn't loaded until one of its names is actually used. The module file still needs to exist; the resolution just happens later.

lazyimport math
lazyfrom math import abs

def main() -> Int32
    print(abs(-7))   # math is loaded here, on first use
    return 0
7

Importing structs

Structs import the same way as functions, and once imported you use them like any local struct.

from geom import Point

p: Point = Point(x=3, y=4)

You can't define a struct and then import it as a function, and vice versa. The compiler checks. Importing a variable is an error:

Error: Cannot import variable 'x' from module 'm' (only functions and structs can be imported)

Using C, C++, Rust, and Python modules

Threadon can call functions and use classes written in other languages. This is done through manifest files, which describe how the external code should be compiled and linked. Four backends are supported: C, C++, Rust, and Python.

The lang field in the manifest selects the backend:

Manifest files

A module written in another language uses a file named manifest to describe how Threadon should load it. The manifest tells the compiler which language the module is written in, where its source file lives, and which functions and classes it exports to Threadon.

A manifest file is a plain text file with one directive per line. Here is an example for a Python module:

module PyAdd
lang python
source padd.py

export add Int64 Int64 Int64
export mean Float64 List[Float64]
export scale List[Float64] List[Float64] Float64
export describe Dict[String,Float64] Dict[String,Float64]

class Vec3 init Float64 Float64 Float64
class Vec3 method length -> Float64

The module name becomes the import name inside Threadon. The lang field picks the backend. The source field points to the file that implements the module.

Each export line declares one function: the name, followed by the return type, then the parameter types. Types can be scalars (Int8…Int64, UInt8…UInt64, Float16…Float64, Bool, String), List[T], or Dict[K,V] with a scalar key. The compiler validates every exported name and type when the module is imported, so an unknown type or a missing function is a compile-time error.

Add compiler flags with flag and extra system libraries with link.

lang c
lang cpp
lang c++
lang rust
lang python

Each backend has its own toolchain:

This makes it possible to write performance-critical code in C or Rust, reuse existing C++ libraries, or call Python code directly from Threadon without rewriting it.

Using classes from foreign modules

In any of the backends, a class directive maps a foreign class to a Threadon struct. The struct has one hidden field holding an opaque handle to the foreign object. Calling the class runs the foreign constructor (with the init argument types), and methods declared with class N method M [Args...] -> Ret become ordinary methods:

from PyVec3 import Vec3

v: Vec3 = Vec3(3.0, 4.0, 0.0)
print(v.length())

For Python, the bridge writes the C++ glue for you, so an ordinary Python class works unchanged — see examples/16_python_classes. For C, C++, and Rust, the same class directive is used, but you provide the glue symbols yourself: the class has the layout struct {Class} { char* __handle; }, the constructor is {Class}___init__ returning that struct, and each method is {Class}___{method} taking the struct as its first argument. These symbols must be extern "C" and use the exact types listed in the manifest. For how these classes behave inside Threadon code, see the Classes page.

Rules

How modules are found

The importer searches these places, in order:

  1. -I DIR directories from the command line, if any.
  2. The folder the main file is in.
  3. The stdlib/ directory that ships with Threadon.

Dotted names map to subdirectories. import a.b looks for a/b.th. A module that needs its own submodules imports them by their full dotted path, resolved against the same search paths.

Because the stdlib is last, a file called std.th in your own project shadows the standard library. That's how the std lib itself is found and overridden.