Skip to content

Modules

StarLang has two types of modules: source modules (pure .star files) and native modules (C-backed with .star stubs).

import

Load modules with import. Each import creates a namespace — access symbols with namespace.symbol().

package main

import "math"
import mqtt "net/mqtt"

fn main():void {
    var x: number = math.sqrt(16.0)
    mqtt.open("host", 1883, "id")
}

Import Syntax

Two forms are supported:

import mqtt "net/mqtt"     # explicit alias
import "net/mqtt"          # auto-alias: last path segment ("mqtt")

Both create a namespace. Symbols are accessed with dot notation:

mqtt.open("host", 1883, "id")
mqtt.publish(conn, "topic", "hello", 0)

Path Resolution

Imports are resolved in two steps:

  1. Local<base_dir>/<module>.star (relative to the importing file)
  2. star.mod — if not found locally, search require paths from star.mod
import "utils"     # → ./utils.star (local)
import "math"      # → star.mod require path → dev-libs/math/math.star (native)

star.mod

Dependencies are declared with require <path> <version> in the project's star.mod manifest:

require ../dev-libs/math v0.1

import "math" then resolves to that path, and math.sqrt(x) compiles to an OP_NATIVE_CALL. Only modules listed in star.mod are available for native import. See the star.mod reference for the full manifest format (module, star, require, config).

Export

The export keyword controls function visibility in modules. Only exported functions are accessible from importers.

package mylib

export fn add(a:i32, b:i32):i32 {
    return a + b
}

fn helper():i32 {
    return 42
}

In this example, add is accessible via mylib.add(), but helper is not visible to importers.

If no function in a module uses export, all functions are visible (backward compatible).

Native Modules

Native modules are implemented in C and exposed to StarLang through stub declarations. A native function stub has the export fn signature but no body:

package math

export fn sin(x: number): number
export fn cos(x: number): number
export fn sqrt(x: number): number
export fn pow(base: number, exp: number): number

The compiler recognizes stubs (no { after return type) and emits OP_NATIVE_CALL instead of a regular function call. Each native is identified by a contract hash computed from its module, symbol, parameter types and return type; the image records the natives it needs in its NREQ section, and the VM resolves each one to a registered C function at load — no string lookup at runtime, and no dependence on the order natives were registered.

Because resolution is by hash, a runtime only needs to register the natives it actually provides. A device firmware (e.g. ESP32) typically registers just the compute subsetmath, time, json, queue, crypto — and omits host I/O modules like http, net, ws, mqtt and config. An image that imports a native the runtime didn't register fails to load with a missing native error listing the unresolved contract hashes, rather than calling the wrong function.

Available Native Modules

Module Functions
math sin, cos, tan, sqrt, abs, floor, ceil, round, pow, log, log10, atan2, min, max

Using Native Modules

  1. Add require to star.mod:

    require ../dev-libs/math v0.1
    

  2. Import and use:

    package main
    
    import "math"
    
    fn main():void {
        var x: number = math.sqrt(16.0)
        var y: number = math.pow(2.0, 10.0)
        console.log(x)
        console.log(y)
    }
    

Source Modules

Regular .star files with function implementations:

package utils

export fn double(x:i32):i32 {
    return x * 2
}

These compile inline into the main bytecode — no separate linking step.

Module Layout

project/
  star.mod
  main.star
  utils/
    helpers.star
dev-libs/
  math/
    math.star       # native stubs
    math.c          # C implementation
    math.h
  gpio/
    gpio.star
    gpio.c
    gpio.h

No Name Collisions

Because all symbols are namespaced, two modules can define the same function name without conflict:

import mqtt "net/mqtt"
import http "net/http"

fn main():void {
    mqtt.open("host", 1883, "id")    # net/mqtt.star → connect()
    http.get("url")   # net/http.star → get()
}

Alias Collisions

Duplicate aliases are a compile error:

import "net/mqtt"       # alias: "mqtt"
import "drivers/mqtt"   # alias: "mqtt" → ERROR: duplicate import alias 'mqtt'

Fix by using explicit aliases:

import net_mqtt "net/mqtt"
import drv_mqtt "drivers/mqtt"

Circular Imports

Circular imports are a compile error:

# a.star imports b.star
# b.star imports a.star → ERROR: circular import 'a'

Restructure your code to break the cycle — extract shared symbols into a third module.

Unused Imports

Importing a module without using any of its symbols is a compile error:

import "math"   # ERROR: 'math' imported but not used

Remove the import or use a symbol from the module to fix.

Rules

Rule Description
Single load Each module is loaded at most once per compilation
Ordering Imports must come after package, before other code
Namespaced All imported symbols require alias.symbol access
Inline compilation Source modules are compiled inline into the main bytecode
Native dispatch Native modules use OP_NATIVE_CALL — direct index, no string lookup
Export visibility If any function has export, only exported functions are importable
star.mod required Native modules must be listed in star.mod with require
No duplicate aliases Two imports cannot share the same alias
No circular imports Circular dependencies are a compile error
No unused imports All imports must be referenced (compile error otherwise)
Debug info Each module retains its own source filename and line numbers
Max imports Up to 32 imports per file (compile error if exceeded)