SultanC
  1. Home
  2. Documentation
  3. Reference

Modules and Imports

Documentation pages

Modules and Imports

This page specifies how SultanC finds, names and connects source modules: the import forms, how an import path resolves to a file, how package manifests contribute root names, and the diagnostics for each failure. It is for programmers who split a program across files and for contributors working on module loading.

Modules

Every .sn file is a module. A module is identified by a canonical key, the pair (root, normalized logical path). The root is the package or library tree the file belongs to, and the logical path is the dotted name of the file below that root, without the .sn extension. The file shapes/area.sn in a package has the logical path shapes.area. Identity never depends on the filesystem path used to reach the file, on the spelling of the root, or on the import form: every import of one file produces the same module key, the same declarations and the same symbols. Module loading owns only this canonical identity; it does not infer identity from file names, which belong to the source locator.

A declaration is visible to other modules only when it is marked public. Anything else is private to its module, and a use from outside is SULTANC-NAME-PRIVATE.

public type Rectangle = { width: int, height: int };

public def area(shape: &Rectangle): int {
    return (*shape).width * (*shape).height;
}

def helper(): int { return 0; }

Only Rectangle and area are visible to an importer of this file.

Import forms

import is spelled استورد in Arabic, and from … import is من … استورد. Either spelling may appear in any file, whatever language the rest of the file is written in.

Quoted relative import

import "geometry.sn";

A quoted path names a file relative to the logical directory of the importing module. It stays inside the same root: a path that climbs out of the root (../x.sn) is rejected, and so is an absolute path.

Named import

import std.text.number;
import util.shapes.area;

A named import is a dotted path of at least two components that ends at a module. The first component decides the root. When it is a registered root spelling (the standard library, the importing package, a dependency, or any of their declared names; see Roots), the rest of the path is resolved under that root. When it is not, the complete path is resolved under the importer's own project root. A path is never retried relative to the importing module's directory, and a lookup that failed under a registered root is never retried against the project tree. A path of a single component is an invalid reference (SULTANC-IMPORT-INVALID-REFERENCE).

Importing a module brings every public name it declares into scope, unqualified. Importing the same module twice is harmless. If two imported modules declare the same public name and the program uses it, the use is ambiguous (SULTANC-IMPORT-AMBIGUOUS); give the imports aliases or select the names you need.

Selective import

from std.text import int_to_text, contains;
from std.io import println;

A selective import names a directory and lists the declarations to bring in; only the listed names enter scope. Name resolution searches the public declarations of the modules in that directory. A listed name that does not exist or is not public is SULTANC-NAME-UNKNOWN, and a name declared in more than one module of the directory is SULTANC-IMPORT-AMBIGUOUS. Module loading only selects the directory and its canonical identity; deciding whether a name exists, is public or is ambiguous belongs to name resolution. The same form works on directories of the project as on those of the library.

Aliased import

import std.text.number as numbers;
import std.io.println;

def main(): int {
    println(numbers.int_to_text(42));
    return 0;
}

as changes only the local binding. The names of the module are reachable through the alias as a qualifier (numbers.int_to_text), and are no longer in scope unqualified. The alias never creates a new module, symbol or source identity. The alias works on quoted and named imports alike, which is how two modules that declare the same name are used together. A qualified name cannot be written in a type position.

C headers and sources

import "sqlite3.h";   // declarations only: link the library with -l sqlite3
import "vec.c";       // declarations, and vec.c is compiled and linked

def main(): int {
    if (sqlite3_libversion_number() < 3000000) { return 1; };
    let v: *mutable vec = vec_new(4);  // struct vec is opaque here: a typed handle
    let ok: i32 = vec_push(v, 7);
    vec_free(v);
    return SQLITE_OK;             // a #define is a constant
}

A quoted path ending in .h or .c imports C through Jisr, SultanC's C frontend (compiler/jisr). The file is looked up next to the importer first, then under $SULTANC_C_INCLUDE (:-separated) and the include directories of the target being compiled ($SULTANC_C_SYSROOT when set; the host's for the host target, the macOS SDK for Darwin, the multiarch directories for a cross Linux target). Jisr preprocesses it itself, with the target's predefined macros (__x86_64__, __aarch64__, __APPLE__, _WIN32, __LP64__, __SIZEOF_LONG__, …) and its own <stddef.h>, <stdarg.h>, <stdint.h>, <limits.h> and the other freestanding headers; parses the declarations with the target's data model (LLP64 on Windows, LP64 elsewhere, ILP32 on 32-bit targets) and record layouts; and writes them as a SultanC module. SULTANC_C_PREPROCESSOR=cc selects the system C compiler's preprocessor instead (cc -E -dD, or $CC); SULTANC_C_DUMP=<file> writes the generated module to a file.

  • functions become public extern def with parameters p0, p1, …, and keep an asm label as their link name;
  • typedefs become type aliases; structs whose C layout is SultanC's natural layout become records (an array member is a tuple); unions, records with bit-fields and packed records become opaque storage of the C size and alignment ({ opaque: (u64, …) }); a record over-aligned beyond 8 bytes has no SultanC form;
  • C objects become external data (public extern let x: T = "x";), read where they are used;
  • enumerators, C23 constexpr integer objects, and object-like macros whose expansion is an integer constant expression, a floating literal or a string literal, become public let constants, typed as C types them (1u << 31 is u32, 0x100000000LL is i64); function-like macros are expanded inside them;
  • the declarations written are those of the imported file and of the other headers under its root: the include directory it was found in (import "sys/socket.h" has AF_INET from the bits/ headers), or the directory of a project file. Other headers are read for their types, enumerators and macros. Names C reserves for the implementation (__x, _X) are never written from a system header; a project file's own are;
  • a C name that is a SultanC keyword or builtin (match, type, truncate, …) gets a trailing _, and a function keeps its C symbol through a link alias (truncate_ calls truncate).

Types: integers and floats keep their width on the target (int is i32, long is i64 on LP64 and i32 on Windows), char * is *mutable u8 (*u8 when const), other pointers are typed (*T, *mutable T, *void, *S, *mutable *mutable u8), and a pointer to a function or to a type with no SultanC form is an address word of the target's pointer width. A struct or union the program sees only through pointers is an opaque type (public type sqlite3 = opaque;, Type System): one only declared (struct X;, typedef struct X X;, a struct X * with no definition), one a header of the import declares incomplete and only a .c unit defines (vec * above: the header is its interface, so import "vec.h" and import "vec.c" write the same type and share it), and one whose layout has no SultanC form. It is ordinary C, so it has no diagnostic. Its pointers are *NAME for const NAME * and *mutable NAME otherwise (a *mutable NAME passes where a *NAME is expected, as C passes T * for const T *), each struct is its own type through its forward declaration, typedefs and definition, a handle of the wrong type is SULTANC-CALL-ARGUMENT-TYPE-MISMATCH, and dereferencing one is SULTANC-TYPE-OPAQUE-DEREFERENCE. A struct defined only in a .c unit stays a record. A C NULL argument or result is 0 as *T. Records pass by value where their C class allows it. _BitInt(N) up to 64 bits is the narrowest integer that holds it, and long double is f64 where the target's long double is double (Apple arm64, Windows). long double wider than double (x87's 80 bits, binary128), __int128 and _BitInt wider than 64 bits (SultanC has no 128-bit integer, and a record standing in for one is passed differently from it on AArch64 and Win64), _Complex, vector types (__attribute__((vector_size(N)))), a declaration without a prototype (f();), thread-local and array objects have no SultanC form. Each such declaration is left out with a // skipped NAME (file:line): c.skipped.… [SULTANC-C-…] comment (the reason's key, not prose), never written with a wrong type, and reported as a warning with that code at its C location (the import still succeeds; only the name is missing). A C name SultanC identifiers cannot spell (café) is left out the same way (SULTANC-C-NAME-CONFLICT-SKIPPED); code of a .c import still uses it.

A variadic function (printf) is written with its fixed parameters and the link name printf...1: a call passes them with the C variadic convention on every target (AL on x86-64 System V, the stack on Apple arm64, floating arguments duplicated in integer registers on Win64). To pass more arguments, declare a call shape with the same SYMBOL...N link name, N being the number of fixed parameters:

extern def print_two(format: *u8, a: i32, b: f64): i32 = "printf...1";

C errors are diagnostics of their own: a C error in the imported file or its project headers (#error, a missing #include, a syntax error, conflicting declarations, a failed _Static_assert, an invalid operand in compiled code) is reported with its SULTANC-C-… code at the C file's line and column, with the C source line, before any SultanC error that follows from the failed import, in English or Arabic like every other diagnostic. Jisr hands over the diagnostic's identity, its C reason and its facts (the C name, type, expected and found token, value, include path); iSpeak words them. A file brought in by #include names the #include lines that led to it, innermost first. C source is UTF-8: bytes that are not (SULTANC-C-INVALID-UTF8) and an identifier character outside C23's XID_Start/XID_Continue (SULTANC-C-INVALID-IDENTIFIER-CHARACTER) are errors where the token is used. An error in a header the import only reads (none of its declarations is written) does not fail the import: it is the warning SULTANC-C-READ-HEADER-UNTRANSLATED, with the C error as a note. Warnings (#warning, declarations left out) are printed when the import succeeds.

When the target's own C headers are not installed, the host's are read in their place when the host has the same data model: inside those headers the host's predefined macros apply, everywhere else the target's (the module says so with SULTANC-C-HOST-HEADERS); otherwise only the freestanding headers are available.

C imports often declare the same thing: stdio.h and stdlib.h both declare size_t, two project headers both include <stdint.h>. A declaration is written once in a program: when another C import of the same source root (for the same target) already exports the same declaration (the same SultanC text, which for records is C's own rule for structs of separate translation units), the generated module imports that one (import "stdio.h"; // declarations this C import shares with it) instead of declaring the name again. The names a C import's module takes this way from another C import's module count among its own names, for every importer (plainly or through an alias): a module that imports only stdlib.h still sees size_t, and one that imports both sees one size_t, not two ambiguous ones. So a record that two .c imports take from a shared header is one type wherever the imports are. A name that two C imports declare differently remains ambiguous where it is used, except that a function one C import declares and a .c import defines is that one function (the compiled definition).

An imported .c file is compiled by Jisr itself, through the ordinary pipeline: its functions and objects become SultanC code of the module (compiler/jisr/lower.sn), so it builds for every target and backend (the native backends, LLVM, the interpreter) with no C compiler, cross targets included. Its non-static function definitions are public def with their C signatures, its non-static scalar objects public let; main is never written, and a prototype of a function it does not define is left to the import that does. Compiled code covers C11 statements and expressions: goto and labels, switch with fallthrough and GNU case ranges, Duff's device, pointer arithmetic, structs, unions and bit-fields by value and in memory, compound literals, designated initializers, static locals, initialized globals, function pointers, statement expressions, _Generic, variadic definitions (va_list, va_arg, va_copy, passed on to vprintf), calls of C library functions (variadic ones included), objects of other files read, written and addressed through their C symbols, C23 constexpr, #embed, typeof, _BitInt(N) (up to 128 bits) and __int128 arithmetic, and these extensions:

  • variable-length arrays (int a[n];, long rows[n][2];): their storage is made where each is declared, every time it is, and sizeof of one is its size at run time;
  • computed goto (labels as values): &&label is a void * naming the label, goto *p continues there, and tables of them (static void *ops[] = { &&a, &&b };) work as in GCC's threaded interpreters;
  • thread-local objects (_Thread_local, __thread, at file scope or static in a block): each thread works on its own copy of their initial values, made on its first use (a copy lasts until the program ends);
  • long double where it is double.

What it cannot compile is reported as SULTANC-C-UNSUPPORTED with its location: asm statements, long double values where long double is wider than double, _BitInt wider than 128 bits, and vector values. A program that writes through C's stdio should call fflush(0) before it returns: a SultanC program does not exit through libc.

The C functions of a program are one set, as after linking: a function that one C import declares (a prototype in a header, or a call in a .c file to a function it does not define) and another .c import of the program defines is called directly, wherever the two imports are, when the declaration's parameter and result types are the definition's; MSIR binds the declaration to the compiled definition (compiler/msir/lowering/functions.sn), and no linker symbol is involved. So .c files imported by different SultanC modules call each other, mutually recursive ones included.

import "a.c+b.c" imports several translation units of one directory as one module: each is preprocessed, parsed and compiled on its own (its statics stay its own), declarations they share are written once, and a call from one to a function another defines is a direct call; a call of another unit's variadic function passes its variadic arguments in 8-byte slots to that unit's compiled function, as a call inside one unit does. A variadic call from one separate .c import to another's function is still a C variadic call of an external symbol, and a pointer to a compiled C function cannot be handed to external C code (qsort, a callback): compiled C functions use SultanC's calling convention, and SultanC has no C-callable entry for a function of any signature.

SULTANC_C_COMPILER=cc compiles imported .c files with the system C compiler instead (cc -c -O2 -fPIC, or $CC), for the host target only: the module then declares their definitions as externs and their objects are linked with the program, with libc added when the command line does not name it. It is never chosen implicitly.

Roots

A root is a tree of modules with a set of spellings that select it. Several spellings can select one root, and they all produce the same module keys.

The standard library is one root with the spellings std and المكتبة; the compiler knows both. Its subdirectories carry per-language spellings as well, so std.io.println, المكتبة.دخ.println and std.entree_sortie.println name one module. The spellings are declared in std's root file, library/std/std.sn (see Module spellings):

Directory Arabic Directory Arabic
io دخ net شبكة
fs ملفات http ويب
text نصوص debug تصحيح
chars حروف meta انعكاس
bytes بايتات error أخطاء
encoding ترميز fmt تنسيق
memory ذاكرة process عمليات
env بيئة crypto تشفير
toml تومل collections مجموعات
version إصدار time وقت

The root file also gives French and Spanish spellings for some directories. The file names inside a directory (println, number) are not translated; they stay as written in both languages. Directories without a spelling, such as std.json, have a single name.

The compiler finds the standard library by its layout, a std/ folder (see Where the compiler looks for std).

Module spellings

A folder's root file declares spellings of the folder's direct children with module (وحدة in Arabic):

module io { ar: دخ, fr: entree_sortie, es: entrada_salida };

The name after module is the child as it is on disk: a NAME.sn file or a NAME folder next to the root file. Each spelling is an identifier and may not be a reserved word. The root file of a folder is FOLDER/FOLDER.sn (never mod.sn); the root file of a package is SOURCE_ROOT/PACKAGE.sn, named after the package (library/std/std.sn for std). A root file may have other declarations as well; module may appear nowhere else.

An import path is read component by component: in المكتبة.دخ.println, المكتبة selects std, std.sn maps دخ to io, and io/io.sn, if it exists, may map println. A component that no root file maps stays as written. A spelling is one more name of one module, never a second module. Each root file is read once per compilation, for its module lines only.

A package contributes its own roots: its name, every one of its spellings, and, for a dependency, the name its edge gives it. The project can therefore import itself by name (import app.helper;), and a dependency by the name it was given.

Packages and the package graph

SultanC does not read Supax.toml. Supax, the package manager, reads and checks manifests, resolves and fetches dependencies, and writes one package graph for the compiler, build/supax/package-graph.toml, which it passes with --package-graph=<file> (also --package-graph <file>) to builds, check, run and test. --package-graph=- (or --package-graph -) reads the graph from standard input instead, as an editor or a tool passes a graph it has not written to a file; the graph is read once per compiler process, and its relative paths (source_root) resolve against the current directory, since standard input has no directory. The compiler never discovers a manifest, never walks dependencies, never reads Supax.lock and never uses the network.

Inside a Supax project, run supax check, supax build or supax run. A plain sultanc check src/main.sn compiles the file standalone: it does not see the project's dependencies or the package's own name, even with a Supax.toml beside it.

Without --package-graph, a compilation is standalone: the files named, their relative imports (import "util.sn";) and the standard library. The project root is the working directory for relative sources, or the sources' common directory. A source file of the standard library itself is compiled as std.

version = 1
root = "app"                      # the package that owns the files being compiled

[[package]]
id = "app"                        # unique and opaque; "std" is reserved for the standard library
source_root = "src"               # absolute, or relative to this file
name = "app"                      # the canonical root spelling
spellings = { ar = "تطبيق" }      # more root spellings, by language
entry = "lib"                     # optional: the module the root spellings name

[[edge]]
from = "app"
to = "json@1.2.0"
name = "json"                     # the dependency's name inside `from`
Entry Content
version 1.
root The id of the package whose source root holds the files named on the command line.
[[package]] id, source_root, name, optional spellings and entry. Modules resolve beneath source_root.
[[edge]] The package to is visible inside from under its name, its spellings and the edge's name.

A package name, every spelling and an edge's name must be identifiers and not reserved words of the language; the compiler checks them with its own keyword list. Every package sees the standard library under the same spellings as the project, and itself under its own names. A graph may not describe std, which the compiler knows itself. A source_root is cleaned lexically, as Cargo does, without following symbolic links: app/../util is util, so source names and diagnostics never carry ... Two packages whose source roots clean to one directory are an error. An error in a graph is reported as SULTANC-PACKAGE-GRAPH-INVALID at its path:line:column, with the line, as an error in a source file is.

Spellings of the modules inside a package are no part of the graph: the package's root file declares them. With the graph above and src/app.sn holding module shapes { ar: أشكال };, the module src/shapes/area.sn is imported as app.shapes.area, تطبيق.shapes.area or تطبيق.أشكال.area, and a module lib.sn of the json package as json.lib.

Diagnostics

Identity Meaning
SULTANC-MODULE-NOT-FOUND No source exists for the selected root and path.
SULTANC-MODULE-INVALID-LOGICAL-PATH A component of a source path is not a valid module name.
SULTANC-IMPORT-INVALID-REFERENCE The import has the wrong shape, such as a single-component path.
SULTANC-IMPORT-CYCLE Modules import each other in a cycle.
SULTANC-IMPORT-AMBIGUOUS Two imports provide the same name, or a selective import matches several modules.
SULTANC-NAME-UNKNOWN A selected name does not exist in the imported scope.
SULTANC-NAME-PRIVATE The name exists but is not public.
SULTANC-PACKAGE-GRAPH-INVALID A package graph is unreadable, malformed, of another version, or names what does not exist; reported at its line and column.
SULTANC-STD-NOT-FOUND No library directory holding std/ where the compiler looks for std.
SULTANC-MODULE-SPELLING-NO-CHILD A module declaration names no file or folder of its folder.
SULTANC-MODULE-SPELLING-SHARED Two children of one folder share a spelling, or a spelling is another child's name.
SULTANC-MODULE-SPELLING-RESERVED A module spelling is a reserved word.
SULTANC-MODULE-OUTSIDE-ROOT-FILE module in a file that is not a folder's or a package's root file.

SULTANC-PACKAGE-MANIFEST-INVALID, SULTANC-PACKAGE-GIT-DEPENDENCY-UNRESOLVED and SULTANC-PACKAGE-DEPENDENCY-MAP-INVALID are retired: sultanc no longer emits them, and Supax reports manifest and dependency errors. A public code is never reused.

A missing project module and a missing module under a registered library root both fail as an absent source for the root that was already selected.

Explicit module sources

--module-source=<module>=<file> changes only where the source of an existing module is read from. The dotted <module> is resolved from the root module exactly as a named import, so every import of that module keeps the same canonical key; the file becomes the explicit source location of that key in the logical environment, which the source locator consults before deriving a path. The trusted standard-library root, direct inputs, unknown modules and repeated bindings are rejected. Builds use it to select compiler-build capabilities (see Compiler-build capabilities).

Limitations

  • A qualified name from an aliased import cannot appear in a type position.
  • A selective import lists plain names; renaming a name with as in the list is not supported.
  • A single-component named import is rejected.

See also