Modules
Module Identity
Section titled “Module Identity”Module identity is determined by the filesystem; there are no module declarations. A file’s module name is its filename minus the .gray extension. A directory’s module name is its directory name.
project/ main.gray ← entry point helpers.gray ← module "helpers" utils.gray ← module "utils" models/ user.gray ← ┐ task.gray ← ┘ merge into module "models" internal/ cache.gray ← separate module "internal"Directory imports merge all top-level .gray files in that directory into a single namespace under the directory’s name. Subdirectories are not included; they are separate modules that must be imported independently. Hidden files (names starting with .) are excluded from directory scans.
All relative import paths are resolved relative to the importing file’s directory, not the entry point file’s directory. A file inside models/ that uses import "./shared.gray" resolves to models/shared.gray; to reach a file in the project root it writes import "../shared.gray".
Imports
Section titled “Imports”Standard library imports are prefixed with @:
import @arrays, @maps, @strings
do main() {
}Local imports use relative string paths. The compiler resolves them in order:
- If the path ends in
.gray, import that file directly. - If the path has no extension, try appending
.gray. If a file exists, import it. - If the path (without extension) is a directory, scan it for all
.grayfiles and merge them into one module. - If none of the above match, the import is rejected as unresolvable.
// import "./helpers.gray" // explicit file import// import "./helpers" // resolves to helpers.gray (file) or helpers/ (directory)// import "./models" // models/ directory, all .gray files merge
do main() {}When both helpers.gray and a helpers/ directory exist, the file takes priority.
If a directory contains no .gray files, it is an error.
Import aliasing: Only local imports may be aliased. A local module’s name comes from the filesystem, where it can collide with another module or fail to be a valid identifier; a standard library module’s name is fixed and unique, so aliasing one is an error.
import @math
do main() { println(math.sqrt(16.0))}// import mymod "./server" // local modules can be aliased: mymod.handle() instead of server.handle()// import m @math // Error: '@math' cannot be aliasedMultiple imports can be comma-separated:
// import @math, "./helpers", "./models"
do main() {}Module name validity: A module name derived from the import path must be a valid Grayscale identifier and must not be a keyword. A file whose name is neither must be imported with an alias:
// import "./my-utils" // Error: 'my-utils' is not a valid identifier// import u "./my-utils" // OK
do main() {}Collision detection: If two different imports resolve to the same module name, it is an error. The user must alias one to disambiguate. A standard library import binds a module name the same way a local import does, so the two can collide:
// Error: both resolve to module name "utils"// import "./utils", "./lib/utils"
// Fix: alias one// import "./utils", lib_utils "./lib/utils"
// Error: both resolve to module name "strings"// import @strings, "./strings"
do main() {}A local module may share a stdlib module’s name as long as the stdlib module is not also imported; the local module is then the only strings in scope.
Directory import semantics:
When a directory is imported, all .gray files within it are merged into a single module namespace (the directory name). The following rules apply:
- Files within the directory may import each other via relative paths (e.g.,
import "./types.gray"). These sibling cross-references are resolved internally and do not create separate namespaces; all symbols remain under the directory’s namespace. - Transitive imports inside directory files follow the same rule as everywhere else: they resolve relative to the importing file’s location.
- If a file inside a directory imports its own parent directory (self-referential import), it is rejected.
- If a directory import is followed by a direct import of a file already in that directory, the compiler warns that the import is redundant; the directory namespace should be used instead.
- If two files in a directory declare the same symbol name, it is a collision error.
Deduplication: If the same file is imported multiple times (e.g., directly by main and transitively through another import), it is only processed once. No error is emitted.
Combined Import and Use
Section titled “Combined Import and Use”The import and use syntax combines importing and using in a single statement:
import and use @arrays
do main() {
}This is equivalent to:
import @arrays
do main() {using arrays}Multiple modules can be combined:
import and use @arrays, @strings
do main() {
}Using Declaration
Section titled “Using Declaration”The using declaration brings module members into scope for unqualified access. It can be placed at file scope or function scope:
File scope: all functions in the file can use unqualified access:
import @stringsusing strings
do main() { println(to_upper("hello")) // OK, strings is in file scope}
do shout(s string) -> string { return to_upper(s) // OK, same file scope}Function scope: only that function can use unqualified access:
import @strings
do main() { using strings println(to_upper("hello")) // OK, strings is in scope here}
do shout(s string) -> string { return strings.to_upper(s) // must qualify; using is not in scope here}Multiple modules can be listed:
do main() {using arrays, strings}If two modules in scope both provide a name (for example arrays.contains and
strings.contains), calling it unqualified is an error (E4031) — write the
call with its module prefix.
Module Member Access
Section titled “Module Member Access”Without using, module members are accessed with dot notation:
import @math
do main() { println(math.sqrt(16.0))}With using:
import @mathusing math
do main() { println(sqrt(16.0))}C Interop
Section titled “C Interop”Grayscale can import C headers and call C functions directly using the extern prefix:
Importing C Headers
Section titled “Importing C Headers”extern import "stdio.h" // system header → #include <stdio.h>// extern import "./mylib.h" // local header → #include "./mylib.h"
do main() {}System headers (no ./ or ../ prefix) emit angle-bracket includes. Local headers emit quoted includes. Multiple C imports can be comma-separated:
extern import "stdio.h", "stdlib.h", "string.h"
do main() {
}C imports can be mixed with Grayscale imports on separate lines:
import @mathextern import "stdio.h"
do main() {
}Calling C Functions
Section titled “Calling C Functions”All C functions are accessed via the extern. prefix:
extern import "stdio.h"
do main() { extern.puts("hello from C") extern.printf("value: %d\n", 42)}The extern. prefix is required at every C call site. using and import ... and use are not allowed with extern import; C symbols must always stay qualified.
Accessing C Constants and Macros
Section titled “Accessing C Constants and Macros”C constants and macros are accessed with the same extern. prefix. Their
value has no Grayscale type of its own, exactly like a C call result, so the
same rule applies — assign it to a type-annotated variable before using it
(see Return types below):
extern import "stdio.h", "stdlib.h"
do main() { mut eof i64 = extern.EOF // -1 mut ok i64 = extern.EXIT_SUCCESS // 0 println(eof) println(ok)}Type Mapping
Section titled “Type Mapping”| Grayscale type | C type | Notes |
|---|---|---|
i8, i16, i32, i64 |
int8_t, int16_t, int32_t, int64_t |
Use i32 for C int |
u8, u16, u32, u64 |
uint8_t, uint16_t, uint32_t, uint64_t |
Use u32 for C unsigned int |
f32 |
float |
Exact match |
f64 |
double |
Exact match |
bool |
bool |
Exact match |
char |
int32_t |
Grayscale uses 32-bit for Unicode |
string |
char* |
Auto-converted when passed to C functions |
^T |
T* |
Direct pointer mapping |
Argument width: an extern. call passes each argument at its Grayscale width and relies on C’s implicit conversion to adjust it to the parameter type. Integer and float literals are i64 and f64, so when the C parameter is narrower — C int, unsigned int, short, float, or size_t on a 32-bit target — the value is silently truncated or narrowed with no check and no panic. Pass i32 / u32 / f32 (or the matching sized type) explicitly to match the C parameter. See Safety below.
String conversion: Grayscale strings are automatically converted to char* when passed to C functions. To convert a C char* return value back to a Grayscale string, use the c_string() builtin:
extern import "stdlib.h"
do main() { mut home string = c_string(extern.getenv("HOME")) println(home)}Callbacks: a Grayscale function can be passed to a C function as a callback with a func-ref (()cmp). Its parameters and return type must have a C layout: numbers, bool, char, u8, and pointers (^T is T*, so ^void or ^i64 fits a void * parameter). A string, array, map, or struct parameter or return type is rejected with E3158.
Return types: a C function’s return type is known only to the C compiler. Grayscale gives the result of an extern. call — and the value of an extern. constant or macro — no type of its own, so it may only be used where the type is supplied or where the raw C value is handled directly:
- as the initializer of a type-annotated declaration whose type C can return directly — a number,
bool,char,u8, or a pointer - as an argument to another
extern.call - through
c_string(), which converts a Cchar*to a Grayscalestring - as the value of a
cast()to one of the annotation-eligible types above
extern import "math.h"extern import "stdlib.h"
do main() { mut x f64 = extern.sqrt(2.0) // annotated declaration println(x) // prints 1.4142135623730951
mut home string = c_string(extern.getenv("HOME")) // text: via c_string() println(home)}Using an extern. call result or constant anywhere else — interpolating it, returning it, passing it to a Grayscale function, combining it in arithmetic, or placing it in an array or struct literal — is a compile error. Assign it to a typed variable first.
Safety
Section titled “Safety”extern import is an opt-out from Grayscale’s memory safety. ASBAM, the pointer escape checks (11.7), and bounds checking reason only about Grayscale code — they cannot analyze a C function, so their guarantees stop at the extern. call. Once a program calls into C, “memory safe by default” no longer holds for anything that crosses the boundary.
- Pointers returned from C are unmanaged. ASBAM does not track them, their lifetime is whatever the C library defines, and dereferencing one carries no nil-check unless you first route it through normal
^Thandling. addr()of a local passed to C is unchecked. If the C function retains the pointer past the enclosing Grayscale scope, the pointee is freed and the retained pointer dangles. The escape checks only match addresses that escape through Grayscale code.- Lifetime, bounds, and freeing across the boundary are the programmer’s responsibility. A C function can free memory Grayscale still references, or write past the end of a buffer passed from Grayscale; neither is checked.
- Numeric arguments narrow silently. Integer and float literals are
i64andf64. When a C function’s parameter is narrower, the argument is passed unchanged and C’s implicit conversion truncates the integer or narrows the float — no diagnostic, no overflow panic, and for most functions no C warning either. The overflow panics and range-checked casts that guard a type-annotated extern return value do not apply to an extern argument.extern.srand(4294967299)seeds with3;extern.abs(3000000001)returns1294967295. Passi32/u32/f32(or the matching sized type) explicitly when the C parameter is narrower than 64-bit.
Restrictions
Section titled “Restrictions”The following Grayscale types cannot be passed to C functions:
i128,i256,u128,u256— C has no 128/256-bit integer types- Arrays and maps — Grayscale-specific types with no C equivalent
- Grayscale structs — pass individual fields instead
- Tagged (payload) enums — destructure with
when/isand pass the payload Error— a Grayscale runtime type with no C representation; passerr.codeorerr.msg
C structs returned from C functions can be passed back to other C functions via __auto_type inference.
Reserved Name
Section titled “Reserved Name”The module name extern is reserved for C interop. Files named extern.gray must use an explicit alias:
// import mymod "./extern.gray" // OK, aliased// import "./extern.gray" // Error: 'extern' is reserved
do main() {}