Modules, Packages, and Publication
Silk code is organized around explicit module sets. A module set is the group of
.slk files the compiler loads, resolves, checks, and builds together. Packages
give those files a public namespace, imports describe dependencies, and manifests
make the package root portable across local workspaces, GitHub releases, npm
packages, and system installs.
This guide is the user-space view of that system. For exact grammar and edge cases, use Packages, Imports, and Exports.
The model#
- A source file is one
.slkfile. - A module is a compile-time namespace. It may be a file namespace, a
header-form
module ...;, or an inlinemodule Name { ... }. - A package is a named collection of modules that share a namespace and a
silk.tomlpackage identity. - A module set is the concrete set of modules loaded for one compiler command.
- A package root is a directory with
silk.toml; this is the unit you publish or depend on.
The most important practical rule: imports resolve against the module set. A package can only be imported after the compiler knows where that package root or source file lives.
File layout#
A reusable package usually looks like this:
logger/
silk.toml
README.md
LICENSE
src/
lib.slk
sinks.slk
defs/
api.slk
examples/
main.slk
tests/
logger_test.slk
Conventions:
src/contains implementation modules.defs/contains prototype/interface modules for public ABI or binary package consumers.examples/contains copyable downstream examples.tests/contains package-local tests.silk.tomldeclares package metadata, sources, dependencies, targets, and the distribution file set.
Defining modules#
Most user-space files start with a package declaration:
package oro::logger;
import { println } from "std/io";
export fn info (message: string) -> void {
println("[info] {s}", message);
}
Rules that keep tooling simple:
package ...;ormodule ...;comes first.- Imports form one contiguous block immediately after the package/module header.
- Exports are explicit.
- Source package declarations use
::namespaces. - Manifest package names are Silk package paths such as
loggerororo::logger. - Quoted import specifiers use filesystem-style paths such as
loggerorlogger/sinks.
Use package ...; for reusable application/library code. Use header-form
module ...; when you need a compile-time-only namespace or module conformance
surface, especially in definition/prototype files.
Inline modules are useful for nested namespaces inside one package:
package oro::logger;
export module level {
export let INFO: int = 20;
export let ERROR: int = 40;
}
Downstream code can then refer to logger::level::INFO after importing the
package's default module namespace.
Import style for user-space code#
Prefer module-specifier imports:
import { println } from "std/io";
import fs from "std/fs";
import logger from "logger";
import { Logger, info as log_info } from "./lib.slk";
Use these forms as your default:
import ns from "specifier";binds a namespace, then callns::symbol(...).import { Name } from "specifier";imports selected exported names.import { Name as LocalName } from "specifier";gives a local alias.import "./file.slk";loads a module for side effects such as prototype conformance without binding names.
Specifier meanings:
./x.slkor../x.slkresolves relative to the importing file.std/ioresolves from the configured stdlib root.loggerresolves through the dependency key or package root namedlogger.logger/sinksresolves to thesinksmodule inside that package root.
String import specifiers are path-like. Use / for module paths inside the
string, not ::. Reserve :: for source namespaces, direct package/symbol
import paths, and qualified names in code.
This form scales well because it works for local files, stdlib modules, and dependencies while keeping aliases explicit.
Std prelude globals#
When the standard library is enabled, the compiler automatically loads
std::runtime::globals. That module exposes a small prelude with using
aliases for common std types and interfaces. User code does not import these
names.
The current global list is:
Boolean
Builder
Capacity
Clear
Deserialize
Drop
Function
IsEmpty
Iterator
Len
Number
Parse
Range
ReadU8
RegExp
ReserveAdditional
Result
Serialize
Sized
String
TrySerialize
WriteU8
Use those names directly. For example, write Result(T, E) rather than
importing std/result only to reach the defining module.
Direct package and symbol imports#
Silk also supports direct import paths:
import oro::logger;
import oro::logger::info;
import ::puts;
These are ABI-oriented imports. They name the package/export path directly, so they are useful for:
- definition/prototype modules that describe an ABI surface,
- FFI wrappers that expose exact linked symbols,
- stdlib/runtime internals,
- and explicit global namespace access such as
::malloc.
Effects:
import oro::logger;imports the package namespace directly.import oro::logger::info;binds the exported symbolinfodirectly in the importing module.import ::puts;resolvesputsfrom the unnamed global package.
For ordinary user-space modules, prefer:
import logger from "logger";
import { info } from "logger";
That keeps low-level ABI paths out of most code and gives you aliases through the same import form.
Exports#
Exports define the public surface other modules may depend on:
package oro::logger;
export enum Level {
Debug,
Info,
Warn,
Error,
}
export struct Logger {
min_level: Level,
}
export fn info (logger: &Logger, message: string) -> void {
log(logger, Level::Info, message);
}
Best practices:
- Export only the surface you intend downstream users to call.
- Keep implementation helpers private by leaving off
export. - Prefer one small public namespace over many unrelated exported names.
- Use a
defs/api.slkprototype file when a package may be consumed as a prebuilt binary.
Manifests and module loading#
silk.toml is the package root manifest:
[package]
name = "logger"
version = "0.1.0"
description = "Small structured logger for Silk examples"
license = "MIT"
repository = "https://github.com/oro-computer/silk-logger"
readme = "README.md"
definitions = ["defs/api.slk"]
[sources]
include = ["src/**/*.slk", "defs/**/*.slk", "examples/**/*.slk"]
[dist]
include = ["silk.toml", "README.md", "LICENSE", "src/**", "defs/**"]
[[target]]
name = "logger"
kind = "static"
entry = "src/lib.slk"
output = "build/liblogger.a"
c_header = "build/logger.h"
[[target]]
name = "logger_demo"
kind = "executable"
entry = "examples/main.slk"
output = "build/logger-demo"
How loading works:
silk build --package .readssilk.tomland loads files selected by[sources].- A dependency with
path = "../logger"is loaded from that directory. - A dependency without
pathis searched through contextualpackages/roots,SILK_PACKAGE_PATH, and installed package roots. - The dependency key maps to a directory under each search root; a key such as
loggermaps tologger/silk.toml, whileoro.loggermaps tooro/logger/silk.toml. - Quoted imports use dependency-rooted module paths:
import logger from "logger";loads the dependency's default module, andimport sinks from "logger/sinks";loads a submodule. - Source modules may still declare a symbol namespace such as
package oro::logger;.
Dependencies#
A package depends on another package through [dependencies]:
[dependencies]
logger = { path = "../logger", version = "^0.1.0" }
Then source can import the dependency through the dependency key:
import logger from "logger";
fn main () -> int {
let l = logger::Logger{ min_level: logger::Level::Info };
logger::info(&l, "hello");
return 0;
}
For local development, prefer path. For vendored dependencies inside a
project, place the package under packages/<dependency-key>/ and omit path.
For published packages, use a version requirement plus a package root
materialized by your chosen distribution system.
Publishing through GitHub#
Silk does not require a Silk-owned registry. A GitHub release can publish the same package root users build locally:
git archive --format=tar --prefix=logger-0.1.0/ v0.1.0 | gzip > logger-0.1.0.tar.gz
Recommended release payload:
silk.tomlsrc/**for source packagesdefs/**for importable public surfaceslib/<target>/**for prebuilt libraries, when shippedshare/man/**ordocs/**for package docs
Consumers can unpack the archive and either:
- use a path dependency,
- vendor it under
./packages/logger, - or add the parent search root to
SILK_PACKAGE_PATH.
Publishing through npm#
npm can act as a transport for a Silk package root. Silk still consumes files from disk; the compiler does not fetch npm packages itself.
Minimal package.json next to silk.toml:
{
"name": "@oro/silk-logger",
"version": "0.1.0",
"description": "Small structured logger for Silk examples",
"files": [
"silk.toml",
"README.md",
"LICENSE",
"src",
"defs",
"lib",
"share"
]
}
After installation, depend on the installed package root by path:
[dependencies]
logger = { path = "node_modules/@oro/silk-logger", version = "^0.1.0" }
If the npm package root is node_modules/@oro/silk-logger but the Silk package
name is logger, either place or symlink the package under a search root as
logger, or use a manifest path dependency directly to the installed package
directory.
Targets and platform selection#
Targets describe build outputs, not imports. A single package can expose one public module surface and build different artifacts:
[[target]]
name = "logger_static"
kind = "static"
entry = "src/lib.slk"
target = "linux-x86_64"
output = "build/linux-x86_64/liblogger.a"
[[target]]
name = "logger_wasi"
kind = "executable"
entry = "examples/main.slk"
target = "wasm32-wasi"
output = "build/logger.wasm"
Inside code, use attr(...) for compile-time selection:
import { println } from "std/io";
attr(os="linux") fn platform_note () -> string { return "linux"; }
attr(os="macos") fn platform_note () -> string { return "macos"; }
attr(target="wasm32-wasi") fn platform_note () -> string { return "wasi"; }
fn main () -> int {
println("{s}", platform_note());
return 0;
}
Use target gates sparingly. Prefer portable stdlib code first, then isolate target-specific code in small modules or functions.
ABI and FFI packages#
When a package exposes native ABI, ship a definition file:
// defs/api.slk
module oro::logger as LoggerAbi;
export struct LoggerHandle;
export fn logger_new () -> &LoggerHandle;
export fn logger_free (handle: &LoggerHandle) -> void;
export fn logger_info (handle: &LoggerHandle, message: string) -> void;
Implementation modules may use ext to bind C symbols or may provide Silk
export fn bodies directly. Binary-only packages ship the defs/ file plus a
compatible artifact in lib/<target>/.
Direct package/symbol imports are appropriate in this layer because the code is describing ABI paths intentionally:
import oro::logger::logger_info;
import ::puts;
User code should normally import the friendly namespace instead:
import logger from "logger";
Best practices#
- Prefer
import ns from "module"for namespaces andimport { Name } from "module"for selected names. - Use direct
package::symbolimports only when naming ABI/export paths is the point. - Keep a package's public surface small and exported deliberately.
- Put reusable public prototypes in
defs/when publishing libraries. - Keep source packages buildable without network access.
- Make GitHub/npm/system packages materialize the same package root.
- Use
[dist]so published files are intentional. - Put platform-specific code behind
attr(os="...")orattr(target="..."), and keep those gates narrow.
Next#
Source repository · Edit this page · View Markdown