Packages, Imports, and Exports
This document specifies the initial surface syntax for packages, imports, and exports in Silk. The semantics are intentionally minimal for now and will be extended as the compiler’s resolver and linker mature.
Notes#
package <path>;declarations (with the module ordering rules below).module <path>;declarations (mutually exclusive withpackage) includingmodule ... as <Interface>;conformance checking.- Inline module declarations (
module Name { ... }/export module Name { ... }) for nested namespaces. - A contiguous top-level
importblock (package imports andfrom "..."module specifier imports). - Named re-exports:
export { Name, Other as Alias };(exports an in-scope value name so other modules may import it). - Package imports (
import std::strings;) that make a package’s exported values available for use in the importing source file. - Package-import aliasing (
import std::window;,import std::strings as str;) that binds a namespace alias for qualified access (window::is_supported,str::eq). - Qualified symbol imports (
import std::strings::Builder;,import std::io::println;,import ::malloc;) that bring a single symbol into scope without importing the entire package namespace. - Module-specifier imports (
import { Name } from "...";,import ns from "...";) including: - relative file imports (
from "./file.slk"), - std package specifier imports (
from "std/strings"; a trailing.slkis accepted for compatibility and stripped before package lookup), - dependency-rooted POSIX module specifiers (
from "logger/lib") resolved through[dependencies]in the importing package manifest; dotted dependency keys such asmy.dep.bmatch quoted paths such as"my/dep/b"by longest prefix, - and unquoted package-path specifiers (
from ns_pkg::subpath). - Default exports (
export default fn ...andexport default Name;) and default imports that bind either: - the default-exported symbol, or
- the module namespace when no default export exists.
- Declaration-only exported function prototypes (
export fn name(...) -> T;) for header-style “prototype modules” that describe an exported surface without providing a body (satisfied by link-time definitions from other Silk sources and/or.o/.ainputs).
Limitations:
- Bulk re-exports (“export from ...”) and forwarding of export surfaces.
- A stable, fully specified “package build” system outside the current CLI/module-set model (see package manifests for current manifest support).
Working examples (recommended to read alongside this doc):
- Package imports:
tests/silk/pass_import_std_strings.slk,tests/silk/pass_import_std_arrays_methods.slk,tests/silk/pass_import_pkg_util.slk - File imports (named + default):
tests/silk/pass_file_import_named_values.slk,tests/silk/pass_file_import_default_export.slk,tests/silk/pass_import_namespace_file_no_default.slk - Package-path imports (
from pkg::name):tests/silk/pass_import_namespace_package.slk,tests/silk/pass_import_named_from_package_spec.slk
When an import fails, the relevant error codes live in diagnostics
(notably E1001–E1006, plus E2003/E2004 for invalid imported names).
Terminology#
- Source file: a single
.slksource file. - Package: a named collection of source files that share a namespace (declared via
package ...;). - Module declaration: a
module ...;header that declares a namespace and a compile-time-only module value, and may declare interface conformance viaas. - Module set: the set of source files the compiler is compiling together for a given command. Package imports can only resolve to packages that exist in this module set.
- Named import:
import { A, B as C } from "...";(introduces unqualified names). - Default import:
import X from "...";(binds either a default export symbol or a namespace, depending on what is imported). - Namespace import: a default import that binds a module or package namespace; you
access its members as
X::Name.
Packages#
A Silk program is organized into packages and source files.
Each source file may declare the package it belongs to using a package
declaration at the top of the file:
package my_app::core;
Rules:
- Each module MAY declare at most one
packagedeclaration. - When present, the
packagedeclaration MUST appear before all other top-level declarations in the module; it is the first declaration in the file. - Package names are sequences of identifiers separated by
::. - As a special case, the keyword
taskis permitted as a::-qualified segment sostd::taskis a valid package name. std::stringsstd::taskmy_app::coreexample- The standard library lives under the reserved
std::namespace, for examplestd::strings,std::memory, etc.
If a source file omits a package declaration, it is treated as belonging to an
implementation-defined default package (for example, the “main” package for
an executable). The exact rules for default packages will be specified as
multi-module builds are implemented.
In the current silk CLI implementation, when building a package via a package
manifest (silk.toml), source files that omit package default to the
manifest’s package.name. See package manifests.
Modules (module)#
module declares a named module namespace and a compile-time-only module
value.
Syntax:
module my_app::core;
module my_app::core as SomeInterface;
Rules:
- A source file MAY declare at most one
moduledeclaration. - A source file MAY declare at most one of:
- a
packagedeclaration, or - a
moduledeclaration. - When present, the
moduledeclaration MUST appear before all other top-level declarations in the source file; it is the first declaration in the file. - Module names follow the same
::-qualified naming rules as packages. - Modules are compile-time-only values: there is no runtime representation for a module value.
- If a module declares
as <Interface>, the compiler MUST validate that the module satisfies the interface surface as specified in interfaces.
Inline modules (module Name { ... })#
In addition to the source file header form (module ...;), Silk supports
inline modules as a nested-namespace mechanism inside a file:
package my_package;
export module inner_module {
export fn hello () -> string {
return "hello world";
}
}
Rules :
- Inline modules MUST appear at top level (not inside function blocks).
- The inline module name is a single identifier.
- The body is a brace-delimited list of top-level declarations; inline modules may be nested.
package, header-formmodule ...;, andimportdeclarations are not permitted inside an inline module body.- Declarations inside an inline module are referenced from outside using
::qualification (inner_module::hello()). - Within an inline module body, direct nominal declarations in that inline
module (
type,struct,enum,error,interface) are available through unqualified type lookup: impl User { ... },new User(),User{ ... }, andtype Alias = UserresolveUserto the current inline module before falling back outward.- other inline-module declarations still require explicit
::qualification in Silk currently. export module Name { ... }exports the namespace:- exported declarations inside it become part of the containing package’s
export surface with their names prefixed by
Name::(for exampleinner_module::hello), - exported type aliases keep the same prefixed type name surface (for
example
users::UserId) and may appear in exported function parameter and result types, - nested
export moduledeclarations extend the prefix (for exampleouter::inner::name). - Imported namespace qualifiers preserve exported inline-module prefixes. For
example, after
import app as a;, exported members ofexport module users { ... }in packageappare available asa::users::Name; afterimport lib from "./lib.slk";binds a file namespace, the same exported inline-module member is available aslib::users::Name.
Source File Header Ordering (Mandatory)#
In each source file, top-level declarations must appear in this order:
- Optional
packageormoduledeclaration (package ...;ormodule ...;). - Zero or more
importdeclarations, as a contiguous block. - All other top-level declarations.
This ordering is enforced by the parser/resolver and keeps dependency structure easy to understand and tooling-friendly.
Imports#
Source files may refer to other packages or modules via import declarations:
package my_app::core;
import std::strings;
fn main () -> int {
return 0;
}
Rules:
importdeclarations MUST appear at top level (not inside functions or blocks).- All
importdeclarations in a module, if any, MUST appear after the optionalpackagedeclaration (if present) and before any other kind of top-level declaration. In other words, imports form a contiguous block at the beginning of the module immediately following the optional package. - An
importpath is a sequence of identifiers separated by::, matching the package naming rules above (including thestd::taskspecial case). - As with expression/type qualified names, an import path MAY start with
::to explicitly name the global namespace (the unnamed package). importdeclarations identify dependencies and bring exported symbols from the imported package into scope in the importing module, subject to the visibility rules below.- Currently:
- importing a package makes its exported
letbindings with explicit type annotations visible as ordinary, unqualified names in the importing module (for example,import util;followed byanswerrefers toutil::answerwhenutilexportslet answer: int = 42;), - imported exported
letbindings are also reachable via qualified names of the formpkg::name(for example,util::answerafterimport util;); both unqualified (answer) and qualified (util::answer) forms are accepted for now, but the qualified form reflects the intended package-namespaced style, - exported functions (
export fn) are callable across packages for the compiler’s current backend subset: - within a package, top-level functions form a shared namespace across all modules in that package (so functions in one module may call functions defined in another module of the same package),
- when a module imports a package, that package’s
export fndeclarations become callable from the importing module, - both unqualified (
foo()) and qualified (util::foo()) call forms are accepted initially for imported exports, matching the current constant-import behavior, though the qualified form reflects the intended package-namespaced style, - this callable subset is limited to the compiler’s current
code generation subset (supported parameters/results, direct calls, and
structured control flow supported by the IR→ELF backend on
linux/x86_64), - struct type names (
structdeclarations) from imported packages are visible in the importing module for the supportedstructsubset: - the qualified form
pkg::Structis always accepted whenpkgis imported, - the unqualified form
Structis accepted when it is unambiguous across the module’s imports and does not conflict with a locally defined struct name, - when multiple imported packages define the same struct name, the unqualified form is rejected as ambiguous and the qualified form must be used,
- enum type names (
enumdeclarations) from imported packages are visible in the importing module for the supported enum subset: - the qualified form
pkg::Enumis always accepted whenpkgis imported, - the unqualified form
Enumis accepted when it is unambiguous across the module’s imports and does not conflict with a locally defined type name, - enum variants are referenced relative to the enum name (
Enum::Variantorpkg::Enum::Variant), - unqualified type names are introduced only by:
- local declarations in the current package, and
- explicit imports (package imports and module-specifier named imports). The checker does not resolve an unqualified type name by scanning every package in the module set for a “unique match”.
- if an imported package does not exist, resolution fails before type-checking (see the resolver).
- a leading
::on a qualified name forces lookup in the global namespace (the unnamed package), bypassing any same-named declarations in the current package or imported packages. This is intended as an explicit escape hatch for shadowing (for example, calling::malloc(...)when the current module also defines or exportsmalloc). The prefix is valid in both expression and type positions, including: - values:
::malloc(...), - types and struct literals:
::Fooand::Foo{...}, - enum variant paths/patterns:
::E::Variant.
Package import aliasing (import pkg; / import pkg as alias;)#
In addition to importing exported symbols, a package import also binds a namespace alias that can be used for qualified access:
import std::window;
fn main () -> int {
if window::is_supported() {
return 0;
}
return 1;
}
Rules :
-
The default alias is the last segment of the imported package path:
-
import std::strings;bindsstrings -
import std::runtime::mem;bindsmem -
You may override the alias with
as:import std::strings as str; -
If the alias would conflict with an existing in-scope name, the import is a compile-time error; use
asto choose a different alias or use the fully qualified package path (for examplestd::strings::eq).
Qualified Symbol Imports#
In addition to importing whole packages, a module may import a single symbol by fully qualifying it:
import std::io::println;
import std::url::URL;
import ::malloc;
Semantics:
- If the import path matches a package name present in the module set, it is a
package import (
import std::io;). - Otherwise, it is treated as a qualified symbol import:
- the compiler finds the longest package-name prefix of the path,
- the remaining suffix is the symbol name within that package (it may contain
::due to exported inline modules), - the symbol is introduced into the importing module under its final path
segment (for example,
printlnforimport std::io::println;). - When the import path begins with
::, the symbol is resolved from the global namespace (the unnamed package) and is not subject to package export gating.
import { Name } from "..."; remains the preferred form when you need to rename
imports (as) or import from a file path.
Global namespace (::name) rules :
- The global namespace is the package formed by modules that have no
package ...;or header-formmodule ...;declaration (their package name is empty). ::NameresolvesNamefrom that global namespace, if a matching declaration exists in the current module set.::Outer::Inner::NameresolvesOuter::Inner::Namefrom that same global namespace (for example, names nested under inline modules in a global module).- Global names are only accessible via the explicit
::prefix; there is no implicit prelude import of the unnamed package. - Separately, when the standard library is enabled (the default), the compiler
provides a small implicit std prelude of selected standard symbols (for
example
Resultand thestd::interfacesinterface names) as specified bystd::runtime::globals. Use--nostdto disable this behavior.
Future extensions may introduce bulk re-exports and more fine-grained import forms beyond the current package/file-path import surface. Such features will be documented here before they are implemented.
Example: a two-module package program#
Two modules can share a package name and export symbols for other packages to use.
// util.slk
package util;
export let answer: int = 41;
export fn add1 (x: int) -> int {
return x + 1;
}
// app.slk
package app;
import util;
fn main () -> int {
// Currently, both unqualified and qualified access are
// accepted after importing a package. Prefer the qualified form to make the
// origin explicit.
if util::add1(util::answer) != 42 {
return 1;
}
return 0;
}
Package imports resolve against the module set#
A package import resolves only if the package exists in the current module set.
This matters most when you use package-path specifiers (from ns_pkg) or when
you expect a package import to find a package that is not otherwise present.
Tooling note (the silk CLI):
- The language semantics are still “imports resolve against the module set”. The CLI grows the module set by loading additional source files.
- In addition to auto-loading
std::...packages from the stdlib root, the CLI MAY load non-std::packages from a package search path when an unquoted package path is imported (for exampleimport api from my_api;). - The package search path is configured via
SILK_PACKAGE_PATH(PATH-like: roots separated by:on POSIX). - A package name like
my_api::coremaps to the filesystem candidate<root>/my_api/core/silk.toml. The first matching manifest in search order is used. - Qualified imports that include extra
::segments (e.g.my_api::core::Thing) are treated as qualified symbol imports: the CLI resolves the longest package prefix that exists (my_api::core, thenmy_api) and loads that package into the module set.
Example: bringing a package into the module set via a file import, then importing the package namespace:
// main.slk
import { answer as ignored } from "./support_pkg_ns_pkg.slk"; // declares `package ns_pkg;`
import pkg from ns_pkg; // now resolves because `ns_pkg` exists in the module set
fn main () -> int {
return pkg::add1(pkg::answer);
}
If you omit the file import (or otherwise fail to include a module that declares
package ns_pkg;), the package import fails with E1001 (“unknown imported package”).
From the CLI, the usual fix is to ensure the missing package’s module(s) are
part of the command’s module set (for example by passing their .slk files to
silk check / silk build, or by adding a file import). See
cli silk and cli examples.
Import Specifier Imports (JS-style)#
In addition to import pkg::name; package imports, Silk supports JS-style
import forms that use from with either a quoted module path or an unquoted
package path.
The current JS-style forms are:
- Named imports:
import { Name } from "<specifier>";orimport { Name } from package::path; - Default imports / namespace imports:
import Name from "<specifier>";orimport Name from package::path; - Ambient imports:
import "<specifier>";
An import specifier string is interpreted in one of three ways:
- File specifier: the string begins with
./or../. These imports resolve to a module by file path. - Std package specifier: the string begins with
std/. These imports normalize/to::and resolve against the linked std package surface, not by direct file path lookup. - Dependency module specifier: any other string is a POSIX-style module path
rooted at a dependency key in the importing package's
silk.toml. For example, with[dependencies] logger = { path = "../logger" },from "logger/lib"resolves to that dependency'slib.slksource module. If the extension is omitted,.slkis appended. A bare dependency key such asfrom "logger"resolves to the dependency's defaultlib.slksource module. Dotted dependency keys map to slash prefixes, so[dependencies] my.dep.b = { path = "../dep-b" }makesfrom "my/dep/b"resolve to thelib.slksource module in../dep-b.
When a dependency entry omits path, lookup is contextual to the importing
package. Relative SILK_PACKAGE_PATH entries, and the default packages/
directory when SILK_PACKAGE_PATH is unset, are resolved from the importing
package root and then from parent package roots up to the root package of the
current graph. This allows dependency manifests to declare their own nested
dependencies without depending on the command's current working directory.
Quoted import specifiers MUST NOT contain ::. Use an unquoted package path
such as from oro::logger when importing by package namespace.
This mirrors the common JS convention that relative file imports must start
with ./ or ../. Silk additionally reserves the std/ prefix for stdlib
package imports backed by the linked stdlib.
Examples (namespace-style imports):
import ui from oro::ui; // package namespace
import helpers from "./helpers.slk"; // file module namespace (if no default export)
import logger from "logger"; // dependency-key root: dependency src/lib.slk
fn main () -> void {
let opts: &ui::WindowOptions = new ui::WindowOptions();
helpers::do_something();
logger::info();
}
Example with dependency-key mapping:
[dependencies]
logger = { path = "../libs/logger", version = "^0.1.0" }
local.math = { path = "../libs/local-math", version = "0.3.0" }
my.dep.a = { version = "^1.2.0" }
my.dep.b = { version = "^2.0.0" }
import logger_file from "logger"; // dependency default source module
import logger_pkg from oro::logger; // package.name from logger's manifest
import { add } from "local/math"; // local.math -> ../libs/local-math
import my::dep::a::transform; // package-search package namespace
import my::dep::b::some_function; // binary package definition + artifact
import { some_function as some_from_defs } from "my/dep/b";
fn main () -> int {
return logger_file::info()
+ logger_pkg::info()
+ add(20, 1)
+ transform(10)
+ some_function()
+ some_from_defs();
}
In this example:
loggeris an explicit path dependency whose package may be namedoro::logger; quoted imports use the local dependency key ("logger"), while unquoted package-path imports useoro::logger.local.mathis an explicit path dependency whose quoted import root is"local/math".my.dep.ais found from the package search path asmy/dep/a.my.dep.bis found from the package search path asmy/dep/b; if it ships one definition file and a compatible native[[artifact]], the unquoted symbol import and exact quoted named import can both be satisfied by that package.
Ambient imports#
An ambient import loads a module into the module set without introducing any imported names into local scope:
import "./my_api.slk";
import "std/io";
Notes:
- Ambient imports use the same specifier interpretation rules as other specifier-based imports:
.//../paths are resolved as file imports,std/<path>is normalized to a std package name,- other strings are dependency-rooted POSIX module paths matched against
[dependencies]keys. Dotted keys match slash-separated path prefixes, and the longest matching key wins. - Ambient imports do not bind a namespace or import any symbols. If you need to call a function or reference a type from the imported module, use a named import, a default import (namespace import), or a package import.
- Ambient imports are useful for declaring dependencies that exist only to:
- satisfy prototype/definition conformance rules (see below), or
- ensure a module is present in the module set so its types and methods are available for type checking and monomorphization.
Named imports#
Named imports import selected exported names directly into the importing module:
import { StringBuilder, write_u8 as writeByte } from "./runtime.slk";
Notes:
- There is no combined
import foo, { bar } from "...";form in the current grammar. Use separateimportdeclarations. - File specifiers should include the
.slkextension explicitly.std/...strings are package specifiers rather than file paths.
Rules:
- File imports MUST appear in the same import-declaration block as package
imports: after the optional
packagedeclaration and before any other top-level declaration. - The
fromkeyword is part of the import syntax. - The
fromspecifier may be either: - a string literal (
from "./file.slk",from "std/io",from "logger/lib"), or - a package path (
from std::io;,from ns_pkg::sub;). - String literal specifiers MUST NOT contain
::;::belongs to package path syntax. - If the specifier is a file specifier, it is resolved relative to the
importing file’s directory.
./and../path segments are permitted. Absolute paths and backslash-separated paths are rejected in source imports. - If the specifier starts with
std/("std/<path>"or"std/<path>.slk"), it is a std package specifier./is normalized to::, and a trailing.slkis accepted for compatibility and stripped before package lookup. For example,from "std/io"resolves packagestd::io. - If the specifier is an unquoted package path, it is interpreted as a
package name (using the same
::-separated syntax aspackagedeclarations) and is resolved via the package graph. - If the specifier is a quoted dependency module specifier, it MUST match a
dependency key from the importing package manifest. Dot-separated dependency
keys match slash-separated import prefixes (
my.dep.bmatches"my/dep/b"), and the longest matching key selects the dependency root. The remaining path names a module under that dependency's source-module directory; when there is no remaining path,lib.slkis used. The dependency key is independent of the dependency manifest'spackage.name; quoted dependency imports use the key, and unquoted package-path imports usepackage.name. - Binary-only dependency packages may omit implementation sources and ship
[package].definitionsplus a compatible native[[artifact]]. In that case, an exact root specifier such asfrom "my/dep/b"can bind named imports from the package's single definition file when the default source module is absent; the package artifact is then linked automatically by package builds for supported targets. - The imported module MAY declare a
packageor omit it. File specifiers refer to the target module by file path, not by package name.
Exported names for named imports:
- Named imports can import:
- exported values:
export fn, namedexport default fn,export let, and exportedextbindings, and - type names:
struct,enum,error, andinterfacedeclarations. In Silk currently, type exports are recorded but not fully enforced for all type declarations; loading a module into the module set makes its type declarations available for type checking when that module’s package is imported (and for some fully-qualified uses in monomorphized declarations). - exported type aliases:
export type ...;, and - exported Formal Silk theories:
export theorydeclarations (importable so they can be applied via#theory Name(args);). implblocks do not introduce importable names directly, but loading the imported module makes its methods available for method-call checking on the corresponding types.
Name binding rules:
- Each entry in the
{ ... }list names one imported symbol. ascan be used to rename an imported symbol (Name as Alias).- For values (
fn/let/ext), this introduces a value alias. - For type names (
struct/enum/error/interface) and exported type aliases (export type), this introduces a localtypealias (transparent: it does not create a new type identity). - For Formal Silk theories (
export theory), this introduces a theory alias. - Imported names are introduced into the importing module as unqualified names (matching the existing behavior for package imports).
- Importing an unknown name from a file is an error.
- Importing the same value name from multiple file imports without aliasing is an error.
- Importing a value name that is already visible in the module (for example via same-package scope or a package import) is treated as a no-op unless it conflicts with a local declaration in the importing module.
- Importing a type name that is already visible in the module is treated as a no-op.
Default imports and namespace imports#
A module may declare a single default export and importing modules may bind that default export with a JS-style default import:
// module.slk
package module;
export default fn () -> int {
return 1 + 2;
}
// main.slk
import foo from "./module.slk";
fn main () -> int {
let value = foo();
if (value != 3) {
return 1;
}
return 0;
}
Rules:
-
Default exports are module-level and are consumed by default imports (
import Name from "<specifier>";). -
A default export may be declared in either of two ways:
-
a default-exported function declaration:
-
export default fn ...(the function name is optional only in this form), -
or a default-export statement:
-
export default Name;(names an in-scope symbol in the current module). -
Default exports may target any top-level symbol kind that can be referenced by name:
-
functions (
fn), -
top-level bindings (
let/const/var), -
external bindings (
ext), -
type aliases (
type), -
nominal types (
struct,enum,error,interface), -
Formal Silk theories (
theory). -
Each module MAY declare at most one default export.
-
A default export is distinct from named exports:
-
export default fn add () -> int { ... }declares a default export whose internal name isaddwithin the module, -
but it does not implicitly create a named export of
addfor other modules. To export it as a named export, writeexport fn add ...(or add an explicit named export form once one exists in the language). -
The function name after
fnis optional only for default exports. When the name is omitted (export default fn () -> ...), the function is anonymous in the surface language and can only be referenced by importing it via a default file import. -
Default imports have two behaviors depending on whether a default export exists:
-
If the imported module declares
export default, the local name binds to that default-exported symbol. -
If the imported module does not declare a default export, the default import becomes a namespace import: the local name refers to the imported module’s namespace and its exported names are accessed via
foo::Name.
In other words: if there is no explicit default export, the module’s namespace is treated as the default export.
- When a default import binds a default export, it introduces a single unqualified name into the importing module:
- if the default export is callable (a
fnor anextfunction), it binds a callable value name (foo()), - if the default export is a type (
struct/enum/error/interface/type), it binds a type name usable in type positions (and as the head of struct literals), - if the default export is a Formal Silk theory, it binds a theory name that
may be applied via
#theory foo(args...);, - if the default export is a non-callable value (
let/const/varor a non-functionext), it binds a value name. When a default import binds a namespace, it does not introduce any unqualified imported names; you must usefoo::Nameto access exported names. - Namespace imports also expose exported inline-module members by keeping the
inline-module prefix after the namespace name, such as
foo::users::make()andfoo::users::User. - Using a namespace import name as a callable (e.g.
foo()) is an error; add an explicitexport defaultto the imported module or use a named import.
Package namespace imports:
- For an unquoted package path (for example
import ui from ui;), the default import binds the package’s default export when the package declares one. Otherwise, it binds a namespace and exported names are accessed viaui::Name.
Exports#
Top-level declarations can be marked as exported using the export
modifier:
package my_app::core;
export fn main () -> int {
return 0;
}
export let answer: int = 42;
Rules:
exportis not allowed inside blocks; it applies only to module-level declarations. Insideimplblocks,publiccontrols method visibility andexportis reserved for static members.- The implementation supports
exporton: - functions (
export fn ...), including a declaration-only prototype form (export fn name(...) -> T;) used for header-style interface modules, letandconstbindings (export let ...,export const ...).extdeclarations (export ext name = ...;),- Formal Silk theories (
export theory Name(...) { ... }), typealiases (export type Name = ...;),structdeclarations (export struct Name { ... }),enumdeclarations (export enum Name { ... }),errordeclarations (export error Name { ... }),interfacedeclarations (export interface Name { ... }),- static members inside
implblocks (impl T { export fn ... }with noselfreceiver). - The
exportmodifier marks a declaration as part of the package’s externally visible surface. The exact visibility rules across packages (including how exports appear in the resolver and back-end symbol tables) will be specified and implemented alongside the package graph in architecture.
Currently, most type names are treated as visible across
module boundaries once the relevant module(s) are loaded into the module set.
The export modifier is still recorded on type declarations so the
package/export model can be tightened later without changing source.
The checker does not treat “globally unique” unqualified type names as implicitly imported: if a type name is not introduced by a local declaration or an explicit import, it is unknown (even if some other package in the module set defines a type with that base name).
Prototype exports (export fn ...;)#
In addition to ordinary function definitions (export fn ... { ... }), a module
may declare a prototype (a declaration without a body) by terminating the
signature with ;:
module bar;
export fn foo (value: string) -> int;
This is the Silk analogue of a C/C++ header prototype or a TypeScript *.d.ts
declaration file:
- Other modules may import the prototype (named import or namespace import) and type-check calls against its signature.
- The prototype itself does not provide an implementation. The symbol must be provided at link time by:
- another Silk source file in the same package that defines
export fn foo ... { ... }, and/or - an object/archive input that defines the symbol (for example a
.o/.aproduced by a C compiler). - Prototype declarations may include Formal Silk contract annotations (
#require/#assure/ contract#theoryuses). This is the visible contract surface for callers; when the implementation is precompiled and the function body is not available in the module set, callers still type-check and may verify call sites against the prototype’s contract surface.
When both a prototype declaration and a source-level implementation are present in the same build/module set, the compiler enforces:
- the signatures match, and
- the implementation package explicitly imports the prototype module (via a file import) so the relationship is declared in source.
Example (consumer imports the prototype):
import { foo } from "./ibar.slk";
export fn main () -> int {
return foo("hello");
}
Example (implementation imports the prototype and provides the body):
module bar;
import "./ibar.slk"; // ambient import; used for conformance only
export fn foo (value: string) -> int {
return 0;
}
This pattern is equivalent in intent to describing the export surface as an
interface and declaring module conformance (module ... as ...), but it is
file-based and designed to support separate compilation + link-style workflows.
Re-export declarations (export { ... };)#
In addition to export fn ... and export let ..., Silk supports exporting an
already in-scope name via a re-export declaration:
import { my_function } from "./module.slk";
export { my_function };
This is the idiomatic way to build “barrel” modules that forward selected exports from other modules.
Rules :
- A re-export declaration must appear at top level and ends with
;. - Each entry in the
{ ... }list names a local in-scope symbol. - The entry may rename the exported name:
export { localName as ExportedName };. - Re-exported names are part of the module/package export surface, so other
modules may import them via
import { Name } from "./barrel.slk";. - Currently,
export { ... }supports values and exported Formal Silk theories (theorydeclarations). It does not export type names.
Notes#
The current compiler front-end:
- parses
packagedeclarations into the AST, - parses
importdeclarations into the AST, - records whether top-level declarations are marked
export: - values (
fn,let,ext), - Formal Silk theories (
theory), - type aliases (
type), - type declarations where supported (
error,interface), - and similarly tracks
exportfor staticimplmembers andexport defaultfor top-level functions.
The type checker partially respects package, import, and export today:
- a multi-module helper (
checker.checkModuleSetWithImports) seeds each module’s top-level environment with exportedletbindings (with explicit type annotations) from any packages it imports, making those constants visible as unqualified names in the importing module, - within a module set, function calls are type-checked against:
- all top-level functions in the current package (across all modules of that package), and
export fndeclarations from any imported packages, while still rejecting calls to non-exported functions across package boundaries.
A package-level resolver now exists in src/resolver.zig and is used by the
ABI build path (silk_compiler_build) to:
- group modules into packages (including an implementation-defined default
package for modules that omit
package), - ensure that every
importrefers to a package that exists in the current module set, - reject cyclic package graphs (e.g.
package aimportingbwhilebimportsa).
Resolver errors are surfaced through libsilk.a as human-readable
errors (for example, "unknown imported package" or
"cyclic package imports"), and are covered by both Zig tests and C99
tests under c-tests/.
In addition to the package graph, the resolver also builds per-package export tables:
- for each package, all
export fnandexport letdeclarations are collected into a symbol list, - duplicate exported names within the same package are rejected, except for the
prototype/definition pairing described above (
export fn name(...) -> T;+export fn name(...) -> T { ... }), which is accepted only when the signatures match, - these export tables are currently used only for consistency checks; the type checker does not yet use them for cross-package name resolution.
Future work will:
- extend the resolver and checker to:
- map imports to concrete modules and exported symbols,
- ensure only exported symbols are visible across package boundaries,
- propagate package and export information into the IR and back-end so that symbol visibility and linkage match these rules.
Common Pitfalls#
- Forgetting semicolons:
packageandimportdeclarations end with;(parse error,E0001). - Imports not at the top: imports must come immediately after the optional
packagedeclaration and before any other top-level declaration (E0001). - Assuming package imports find code automatically: a package import can only
resolve if the package exists in the module set (fix by adding the relevant
.slkfiles to the build, or by file-importing them; missing packages areE1001). - Calling a namespace import: if
import foo from "./mod.slk";binds a namespace (because there is no default export), thenfoo()is invalid; usefoo::Nameor addexport default(E2018). - Name collisions with named imports: when importing from multiple modules, use
asto rename one binding (E2004).
Source repository · Edit this page · View Markdown