Module System
MathViz provides a module system for organizing code into reusable,
maintainable components. Modules compile to Python files with functions
becoming top-level definitions or @staticmethod methods in classes.
This guide covers everything from basic module creation to advanced dependency
patterns, with real compiled output from the MathViz compiler.
1. Module Basics
A module in MathViz is simply a .mviz file containing functions,
constants, and types. Any .mviz file can serve as a module that
other files import from.
// math_utils.mviz - A reusable module
pub fn factorial(n) {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
pub fn fibonacci(n) {
if n <= 1 {
return n
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
pub fn gcd(a, b) {
if b == 0 {
return a
}
return gcd(b, a % b)
}
fn private_helper() {
return 42
}
import numpy as np
from numba import jit, njit, vectorize, prange
import numba
@njit(cache=True)
def factorial(n):
if (n <= 1):
return 1
return (n * factorial((n - 1)))
@njit(cache=True)
def fibonacci(n):
if (n <= 1):
return n
return (fibonacci((n - 1)) + fibonacci((n - 2)))
@njit(cache=True)
def gcd(a, b):
if (b == 0):
return a
return gcd(b, (a % b))
@njit(cache=True)
def private_helper():
return 42
Each .mviz file is a module. The module name is derived from the filename
(without extension). A file named math_utils.mviz creates a module
named math_utils.
2. Public / Private Visibility
By default, all items in a module are private. Use the pub
keyword to make functions, constants, structs, and traits available for import.
| Declaration | Visibility | Importable? |
|---|---|---|
pub fn name() | Public | Yes - other modules can import it |
fn name() | Private | No - only accessible within the file |
pub const X = 42 | Public | Yes |
const X = 42 | Private | No |
pub struct Point | Public | Yes |
struct Point | Private | No |
pub trait Shape | Public | Yes |
Visibility Example
// Public - can be imported by other modules
pub fn factorial(n) {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
// Private - internal implementation detail
fn private_helper() {
return 42
}
@njit(cache=True)
def factorial(n):
if (n <= 1):
return 1
return (n * factorial((n - 1)))
@njit(cache=True)
def private_helper():
return 42
In the compiled Python, both pub fn and fn produce
identical function definitions. The visibility distinction is enforced at
compile time by the MathViz type checker, not at runtime.
3. Importing Modules
Use the use keyword to import a module. Functions are then accessed
via qualified names (module_name.function_name).
use math_utils
fn main() {
let f = math_utils.factorial(10)
let fib = math_utils.fibonacci(20)
let g = math_utils.gcd(48, 18)
print(f)
print(fib)
print(g)
}
import numpy as np
from numba import jit, njit, vectorize, prange
import numba
import math_utils
@njit(cache=True)
def main():
f = math_utils.factorial(10)
fib = math_utils.fibonacci(20)
g = math_utils.gcd(48, 18)
print(f, end='')
print(fib, end='')
print(g, end='')
if __name__ == "__main__":
main()
The CLI command uv run mathviz compile main.mviz generates
import math_utils (Python-style import). For full module inlining
(where the module code is embedded in the output), use the
CompilationPipeline API programmatically.
Import Patterns
// Basic module import
use math_utils
// Import specific items
use math::{sin, cos, PI}
// Import everything (glob)
use geometry::*
// Import with alias
use statistics::mean as average
// Declare a submodule
mod helpers
4. Module Resolution
When the compiler encounters a use statement, it searches for the
module file in a specific order.
Resolution Order
- Same directory - Look for
module_name.mvizin the same directory as the importing file - Project root - Look in the project's root directory
- Standard library - Built-in modules (
math,collections) - Installed packages - External packages
File to Module Mapping
| File Path | Module Name | Usage |
|---|---|---|
math_utils.mviz | math_utils | use math_utils |
graph/bfs.mviz | graph::bfs | use graph::bfs |
graph/mod.mviz | graph | use graph |
sorting/tri_bulles.mviz | sorting::tri_bulles | use sorting::tri_bulles |
The mod.mviz Convention
For directories with multiple modules, create a mod.mviz file
that serves as the entry point and re-exports items:
// Declare submodules
mod point
mod vector
mod shapes
// Re-export commonly used items
pub use point::Point
pub use vector::Vector
pub use shapes::{Circle, Rectangle, Triangle}
5. Multi-Module Projects
Project Structure
my_project/
├── main.mviz # Entry point
├── math_utils.mviz # Math utility module
├── graph/
│ ├── mod.mviz # Graph module entry
│ ├── bfs.mviz # BFS algorithm
│ ├── dfs.mviz # DFS algorithm
│ └── dijkstra.mviz # Dijkstra's algorithm
├── sorting/
│ ├── tri_bulles.mviz # Bubble sort
│ └── tri_rapide.mviz # Quick sort
└── scenes/
├── mod.mviz # Scene module entry
├── intro.mviz # Intro animation
└── demo.mviz # Demo animation
Multi-File Example
// math_utils.mviz
pub fn factorial(n) {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
pub fn fibonacci(n) {
if n <= 1 {
return n
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
pub fn gcd(a, b) {
if b == 0 {
return a
}
return gcd(b, a % b)
}
// main.mviz
use math_utils
fn main() {
let f = math_utils.factorial(10)
let fib = math_utils.fibonacci(20)
let g = math_utils.gcd(48, 18)
print(f)
print(fib)
print(g)
}
import numpy as np
from numba import jit, njit, vectorize, prange
import numba
import math_utils
@njit(cache=True)
def main():
f = math_utils.factorial(10)
fib = math_utils.fibonacci(20)
g = math_utils.gcd(48, 18)
print(f, end='')
print(fib, end='')
print(g, end='')
if __name__ == "__main__":
main()
6. Compiled Output
Understanding how modules compile to Python is essential for debugging and interoperability. The compiler uses two strategies depending on the compilation mode.
CLI Compilation (import statement)
When compiling with uv run mathviz compile main.mviz, the compiler
generates a standard Python import statement:
use math_utils
fn main() {
let f = math_utils.factorial(10)
}
import math_utils
@njit(cache=True)
def main():
f = math_utils.factorial(10)
Pipeline Compilation (inlined module)
When using the CompilationPipeline API, the module code is
inlined directly into the output file as a Python class with
@staticmethod methods:
import numpy as np
from numba import jit, njit, vectorize, prange
import numba
# Inlined module: math_utils
class math_utils:
@staticmethod
@njit(cache=True)
def factorial(n):
if (n <= 1):
return 1
return (n * math_utils.factorial((n - 1)))
@staticmethod
@njit(cache=True)
def fibonacci(n):
if (n <= 1):
return n
return (math_utils.fibonacci((n - 1))
+ math_utils.fibonacci((n - 2)))
@staticmethod
@njit(cache=True)
def gcd(a, b):
if (b == 0):
return a
return math_utils.gcd(b, (a % b))
def main():
f = math_utils.factorial(10)
fib = math_utils.fibonacci(20)
g = math_utils.gcd(48, 18)
print(f)
print(fib)
print(g)
if __name__ == "__main__":
main()
When modules are inlined as classes, recursive function calls must use the
fully qualified class name (e.g., math_utils.factorial(n-1)).
The compiler handles this automatically when inlining. If you edit the
generated Python, ensure recursive calls use the class prefix.
7. Transitive Dependencies
When module A imports module B, and module B imports module C, this creates a transitive dependency chain. The compiler resolves these by processing modules in dependency order.
main.mviz --> graph.mviz --> queue.mviz
| |
+--------> sorting.mviz |
| |
+-- uses queue --+
// queue.mviz - Base module
pub fn create_queue() {
return []
}
pub fn enqueue(queue, item) {
queue.append(item)
return queue
}
pub fn dequeue(queue) {
return queue.pop(0)
}
// graph.mviz - Uses queue
use queue
pub fn bfs(graph, start) {
let visited = {}
let q = queue.create_queue()
queue.enqueue(q, start)
// ... BFS algorithm
return visited
}
// main.mviz - Uses graph (and transitively, queue)
use graph
fn main() {
let g = [[1, 2], [0, 3], [0], [1]]
let result = graph.bfs(g, 0)
print(result)
}
The CompilationPipeline currently handles one level of module
inlining. For transitive dependencies (A uses B uses C), B is inlined but
C may need to be manually resolved. The CLI command always generates Python
import statements, which Python resolves at runtime.
8. Diamond Dependencies
A diamond dependency occurs when two modules both depend on a common third module:
main.mviz
/ \
graph.mviz sorting.mviz
\ /
queue.mviz
The compiler handles diamond dependencies correctly by ensuring each module
is only compiled and inlined once. In CLI mode, Python's import system
naturally deduplicates imports (each module is loaded once into
sys.modules).
In pipeline mode, the compiler tracks which modules have been inlined and skips duplicates, ensuring the generated output contains only one definition of each class.
9. Circular Dependency Detection
Circular dependencies (A uses B, B uses A) are detected at compile time and produce a clear error message:
// module_a.mviz
use module_b
pub fn func_a() {
return module_b.func_b()
}
// module_b.mviz
use module_a // ERROR: circular dependency!
pub fn func_b() {
return module_a.func_a()
}
error[E0200]: circular dependency detected
--> module_b.mviz:1:1
|
1 | use module_a
| ^^^^^^^^^^^^
|
= help: module_a -> module_b -> module_a
= help: break the cycle by extracting shared code into a third module
How to Fix Circular Dependencies
Extract the shared functionality into a new module that both can import:
// Before (circular):
// module_a -> module_b -> module_a
// After (fixed):
// module_a -> shared
// module_b -> shared
shared/
├── shared.mviz # Common functionality
├── module_a.mviz # Uses shared
└── module_b.mviz # Uses shared
10. Mixing Python and MathViz Imports
MathViz modules can coexist with Python packages. When the compiler encounters
a use statement it cannot resolve as a .mviz file, it
falls back to generating a Python import statement.
// MathViz module (resolved as .mviz)
use math_utils
// Python packages (passed through as imports)
use numpy
use scipy
fn main() {
let f = math_utils.factorial(5)
let arr = numpy.array([1, 2, 3])
}
import math_utils
import numpy
import scipy
def main():
f = math_utils.factorial(5)
arr = numpy.array([1, 2, 3])
if __name__ == "__main__":
main()
You rarely need use numpy because the compiler automatically
converts math functions (sin, cos, sqrt, etc.) to np.xxx and
adds the import numpy as np import. Direct use numpy
is only needed for functions the compiler does not auto-convert.
11. Complete Multi-File Example
Here is a complete multi-file project demonstrating the module system end-to-end: a graph theory module with a BFS algorithm, used by a main file.
Project Structure
graph_project/
├── graphe.mviz # Graph data structure module
├── graphe_bfs.mviz # BFS algorithm (uses graphe)
└── main.mviz # Entry point (uses both)
Module: graphe.mviz
// Graph utilities module
pub fn create_adj_list(num_vertices) {
let adj = []
for i in 0..num_vertices {
adj.append([])
}
return adj
}
pub fn add_edge(adj, u, v) {
adj[u].append(v)
adj[v].append(u)
return adj
}
pub fn vertex_count(adj) {
return len(adj)
}
pub fn edge_list(adj) {
let edges = []
for u in 0..len(adj) {
for v in adj[u] {
if u < v {
edges.append([u, v])
}
}
}
return edges
}
Module: graphe_bfs.mviz
use graphe
pub fn bfs(adj, start) {
let n = graphe.vertex_count(adj)
let visited = [false for _ in 0..n]
let order = []
let queue = [start]
visited[start] = true
while len(queue) > 0 {
let current = queue.pop(0)
order.append(current)
for neighbor in adj[current] {
if not visited[neighbor] {
visited[neighbor] = true
queue.append(neighbor)
}
}
}
return order
}
Entry Point: main.mviz
use graphe
use graphe_bfs
fn main() {
let adj = graphe.create_adj_list(6)
graphe.add_edge(adj, 0, 1)
graphe.add_edge(adj, 0, 2)
graphe.add_edge(adj, 1, 3)
graphe.add_edge(adj, 2, 4)
graphe.add_edge(adj, 3, 5)
let order = graphe_bfs.bfs(adj, 0)
print(order)
}
Compiled Output (Pipeline mode, abbreviated)
import numpy as np
from numba import jit, njit, vectorize, prange
import numba
class graphe:
@staticmethod
def create_adj_list(num_vertices):
adj = []
for i in range(0, num_vertices):
adj.append([])
return adj
@staticmethod
def add_edge(adj, u, v):
adj[u].append(v)
adj[v].append(u)
return adj
@staticmethod
def vertex_count(adj):
return len(adj)
class graphe_bfs:
@staticmethod
def bfs(adj, start):
n = graphe.vertex_count(adj)
visited = [False for _ in range(0, n)]
order = []
queue = [start]
visited[start] = True
while (len(queue) > 0):
current = queue.pop(0)
order.append(current)
for neighbor in adj[current]:
if (not visited[neighbor]):
visited[neighbor] = True
queue.append(neighbor)
return order
def main():
adj = graphe.create_adj_list(6)
graphe.add_edge(adj, 0, 1)
graphe.add_edge(adj, 0, 2)
graphe.add_edge(adj, 1, 3)
graphe.add_edge(adj, 2, 4)
graphe.add_edge(adj, 3, 5)
order = graphe_bfs.bfs(adj, 0)
print(order)
if __name__ == "__main__":
main()
12. Best Practices
One Concept Per File
Keep each module focused on a single domain: one struct, trait, or set of related functions. This makes code easier to find and maintain.
Explicit Exports
Only mark items as pub that are intended for external use. Keep implementation details private to maintain a clean API surface.
Use mod.mviz for Directories
Create a mod.mviz in module directories to serve as the public interface. Re-export commonly used items for convenience.
Avoid Circular Dependencies
Structure modules in a hierarchy where dependencies flow one direction. Extract shared code into a common module.
Group Imports by Source
Organize import statements: standard library first, then project modules, then local modules. Add blank lines between groups.
Test Modules Independently
Each module should be compilable and testable on its own. Avoid tight coupling between modules.
Import Style Guide
// Good: Group imports by source
// Standard library
use math::{sin, cos, PI}
use collections::HashMap
// Project modules
use models::User
use services::{AuthService, ApiClient}
// Local modules
use self::helpers::validate
// Bad: Mixed, unorganized imports
use services::AuthService
use math::sin
use self::helpers::validate
use models::User
use math::cos
Navigation
Previous
Language Reference - Complete syntax documentation
Next
CLI Reference - Command-line interface
Related
Advanced Examples - Multi-module projects in action
Getting Started
Getting Started - Installation and first program