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 defwith parametersp0,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
constexprinteger objects, and object-like macros whose expansion is an integer constant expression, a floating literal or a string literal, becomepublic letconstants, typed as C types them (1u << 31isu32,0x100000000LLisi64); 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"hasAF_INETfrom thebits/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_callstruncate).
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, andsizeofof one is its size at run time; - computed
goto(labels as values):&&labelis avoid *naming the label,goto *pcontinues 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 orstaticin 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 doublewhere it isdouble.
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 buildorsupax run. A plainsultanc check src/main.sncompiles the file standalone: it does not see the project's dependencies or the package's own name, even with aSupax.tomlbeside 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 = "[email protected]"
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
asin the list is not supported. - A single-component named import is rejected.