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
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
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:
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:
- C modules compile to a shared library with
gcc. - C++ (
lang cpporlang c++) modules useg++and can map external classes to Threadon structs. - Rust modules are built with
rustc(--crate-type cdylib) and expose functions and classes through C-compatible symbols. - Python modules are wrapped automatically using the Python bridge, so Python functions and classes behave like native Threadon functions and classes.
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
- No wildcard imports.
from m import *is rejected: "Wildcard imports (*) are not allowed". - No trailing commas in the import list.
- Names must exist. Importing a member that isn't in the module is an error: "Module 'm' has no member 'x'".
- No circular imports. If
aimportsbandbimportsa, you get: "Circular import detected: a -> b -> a".
How modules are found
The importer searches these places, in order:
-I DIRdirectories from the command line, if any.- The folder the main file is in.
- 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.