std::interfaces
This module defines small,
non-generic standard-library interfaces (“protocols”) that can be used today to
express common capabilities across std:: types.
The current compiler supports a closed-world runtime interface subset. Runtime interface values are compiled as unions of the known concrete conformers in the current build, and interface method calls on those values are rewritten into ordinary union dispatch. The compiler still does not expose a separate boxed trait-object / vtable ABI. in Silk, interfaces are used for:
- declaring interface contracts, and
- compile-time conformance checking via
impl Type as Interface { ... }. - closed-world runtime interface values such as
Interface[]when the conformer set is known to the compiler. - empty-method-set interface values in those same runtime positions when the conformer set is known to the compiler.
- one compiler-backed convention:
std::interfaces::Dropis used for automatic cleanup of values at well-defined points (see “Drop semantics” below).
Closed-world runtime model and ABI boundaries#
std::interfaces participates in the same interface model described by
interfaces:
- inside one compiler invocation, an interface-typed runtime value is lowered to a union of the concrete conformers known in that build,
- method calls on that value are rewritten into ordinary
matchdispatch over that union, - there is no general boxed trait object or stable vtable object layout behind
std::interfacestoday.
Implications:
std::interfacesis appropriate for source-level contracts and for closed-world runtime polymorphism inside one build.std::interfacesis not, by itself, a public binary extension mechanism for separately compiled libraries, plugins, or the C embedding ABI.- Do not design a public
libsilk.aor FFI-facing boundary around “pass me anyDrop/Len/Serializeobject” and assume the compiler defines one stable runtime object shape for that. - If you need open-world binary polymorphism, design it explicitly:
- use concrete enums/unions when the set of cases belongs to the API author,
- use concrete structs when a shared data layout is enough,
- or use an explicit function-table ABI when you need dynamic dispatch across separate build products.
When the standard library is enabled (the default), all interfaces in
std::interfaces are available without explicit imports via the std prelude
module std::runtime::globals, so you can write impl T as Drop { ... }.
See also:
- interfaces (syntax, conformance, closed-world runtime dispatch status)
- structs impls layout (method +
exportrules)
Exported API#
std/interfaces.slk currently defines the following interfaces:
module std::interfaces;
interface Drop {
fn drop () -> void;
}
interface Len {
fn len () -> i64;
}
interface Capacity {
fn capacity () -> i64;
}
interface IsEmpty {
fn is_empty () -> bool;
}
interface Sized {
fn size () -> usize;
}
interface Clear {
fn clear () -> void;
}
interface ReserveAdditional {
fn reserve_additional (additional: i64) -> std::memory::OutOfMemory?;
}
interface WriteU8 {
fn write_u8 (value: u8) -> std::memory::OutOfMemory?;
}
interface ReadU8 {
fn read_u8 () -> u8?;
}
interface Iterator(T) {
fn next () -> T?;
}
interface Serialize(S = string) {
fn serialize () -> S;
}
interface TrySerialize(E, S = std::strings::String) {
fn try_serialize () -> std::result::Result(S, E);
}
interface Parse(E, S = string) {
fn parse (value: S) -> std::result::Result(Self, E);
}
interface Deserialize(S = string) {
fn deserialize (value: S) -> Self;
}
interface Builder {
fn run (package_root: string, action: string) -> Promise(int);
}
Notes:
- Most of these interfaces intentionally avoid generics; they are meant to be
usable within the Supported forms.
Serialize,TrySerialize,Parse, andDeserializeare generic, but default their representation type parameter to the common textual case so most callers do not need explicit type arguments. ReserveAdditionalandWriteU8returnstd::memory::OutOfMemory?so allocation-backed types can report allocation failure as a recoverable value instead of trapping.Iterator(T)is modeled after Rust’sIteratorand represents a sequential producer of values. Implementations typically use a receiver of the formpublic fn next (mut self: &Type) -> T?, so callingnextrequires an explicit mutable borrow at the call site:it.next().for x in it { ... }can also be used whenit.next() -> T?; the loop evaluates the iterator expression once and callsnext()repeatedly untilNone(see flow for).- Most interfaces use an implicit receiver: the interface method signature
omits
self, and the correspondingimplmethod includesselfas its first parameter (see interfaces). - Exception:
Deserialize(S)is a static protocol used byascasts; itsimplmethod does not take aselfreceiver and is called asType.deserialize(value). Parse(E, S)is also a static protocol:impl T as Parse(E, S)providesfn parse(value: S) -> Result(Self, E)with noselfreceiver,- calls use
T.parse(value), - unlike
Deserialize,Parseis not used byascasts. - Implemented (partial):
sizeof <string value>yields the string byte length (see operators). - Planned (general):
Sizedwill be used by thesizeofoperator for other value operands: when a concrete type providesfn size(self: &T) -> usize,sizeof valuewill lower to that method call. Serializeis also recognized by theascast operator:- when a type provides
serialize(self: &T) -> S, an explicit castvalue as Slowers tovalue.serialize()(see operators). - Current stdlib adopters of
Serialize(string)include: std::strings::Stringstd::path::PathBufstd::url::URLSearchParamsstd::ffi::c_owned::OwnedCStrThese types already own stable byte storage and can return an allocation-free borrowedstringview.TrySerialize(E, S)is the fallible output-side companion toSerialize:impl T as TrySerialize(E, S)providesfn try_serialize(self: &T) -> Result(S, E),- calls use
value.try_serialize(), - unlike
Serialize,TrySerializeis not used byascasts. - Current stdlib adopters of
TrySerialize(std::memory::OutOfMemory)include: std::strings::Stringstd::path::PathBufstd::url::URLstd::url::URLSearchParamsstd::semver::Versionstd::uuid::UUIDstd::ffi::c_owned::OwnedCStrThese types expose a canonical owned textual rendering, but that rendering may allocate and therefore must remain recoverably fallible.Deserializeis also recognized by theascast operator:- when a type provides
deserialize(value: S) -> Self, an explicit castvalue as Tlowers toT.deserialize(value)(see operators). Deserializemust remain infallible. Fallible parsing or allocation-backed constructors should useParse(E, S)or explicitResult(...)-returning APIs rather than forcing a trap-heavyDeserializeimpl.- Current stdlib adopters of
Parseinclude: std::strings::Stringstd::path::PathBufstd::url::URLstd::url::URLSearchParamsstd::semver::Versionstd::uuid::UUIDThese types can now expose a consistent receiverless parse surface without overloading the cast operator.- Current stdlib adopters of the core container/view protocols include:
std::arrays::Slice(T)andstd::arrays::ByteSliceimplementLenandIsEmpty, whileSliceIter(T)andByteSliceIterimplementIterator(...).std::buffer::BufferU8implementsLen,Capacity,IsEmpty,Clear,ReserveAdditional,WriteU8, andDrop; the lower-levelstd::buffer::Buffer(T)implementsCapacityandDrop.std::vector::Vector(T)implementsLen,Capacity,IsEmpty,Clear,ReserveAdditional, andDrop. Itsiter()method returns the sharedstd::arrays::SliceIter(T)iterator instead of introducing a vector-only iterator protocol surface.std::queue::{FIFOQueue(T), FixedFIFOQueue(T), LIFOQueue(T), FixedLIFOQueue(T)}implementstd::queue::Queue(T)plus the shared container protocolsLen,Capacity,IsEmpty,Sized,Clear,ReserveAdditional, andDrop; they also provide both direct destructiveIterator(T)conformance and non-destructiveiter()snapshots viastd::queue::QueueIter(T).std::stack::{Stack(T), FixedStack(T)}andstd::list::{List(T), FixedList(T)}build on that same queue core and implement the same shared container protocols while adding their stack/list-specific alias methods (top/bottom,first/last).std::map::HashMap(K, V)implementsLen,Capacity,IsEmpty,Clear,ReserveAdditional, andDrop;TreeMap(K, V)implementsLen,IsEmpty,Clear, andDrop; both iterator types implementIterator(Entry(K, V)).std::set::SetMap(T)implementsLen,Capacity,IsEmpty,Clear,ReserveAdditional, andDrop;TreeSet(T)implementsLen,IsEmpty,Clear, andDrop; both iterator types implementIterator(...).std::fs::FileimplementsDrop,std::fs::DirimplementsIterator(DirEntryResult)andDrop, andstd::fs::MMapimplementsLen,IsEmpty, andDrop.- Current regression coverage for that protocol story is intentionally split:
tests/silk/pass_std_interfaces_core_containers.slkis the focused shared smoke test for in-memory/container protocol surfaces, including representativefor-loop iterator consumption for both empty and populated slices, bytes, vectors, maps, sets, and the emptyMMapbyte view. Each representative iterator family in that fixture is exercised through both iterator call expressions and iterator value bindings, with the empty and populated checks split across the in-memory/container set.tests/silk/pass_std_fs_file_drop_and_helpers.slkis the stronger end-to-end regression forstd::fs::File as Drop, including proof thatdrop()actually closes the saved OS file descriptor.tests/silk/pass_std_fs_file_drop_basic.slkis the narrower companion pin for post-drop invalidation and idempotent cleanup on invalid handles.- Compiler-inserted
Dropglue forstd::fs::Fileis covered separately bytests/silk/pass_drop_scope_exit_file_close.slk,tests/silk/pass_drop_overwrite_file_close.slk,tests/silk/pass_drop_heap_ref_file_close.slk, andtests/silk/pass_drop_overwrite_heap_ref_file_close.slk. tests/silk/pass_std_fs_read_dir_basic.slkremains the dedicated end-to-end regression forstd::fs::Dir.next()and real directory-handle iteration.- Stdlib conversion convention:
Serialize(string)is reserved for infallible textual views of the current value. In practice that means stable, allocation-free borrows such asString,PathBuf,URLSearchParams, andOwnedCStr.TrySerialize(E, std::strings::String)is the canonical fallible owned-text rendering path for values whose string form may allocate.Parse(E, string)is reserved for self-contained values that can be constructed from a single textual representation without extra ownership or mode choices.- Structured text formats that need explicit parse modes or format-specific
emission stay on explicit APIs instead of forcing those semantics into the
builtin interfaces. Current examples are
std::json::Documentandstd::toml::Document, which useparse(...)/parse_owned(...)and JSON-specificstringify(...)APIs rather thanSerialize(string)or a blanketParse(...)impl. Builderis the standard interface forbuild.slkbuild modules used by thesilkCLI (see build scripts). It is a module-level interface (used viamodule ... as ...) and defines a singlerunentrypoint that may be implemented asasyncandawaited by the driver wrapper.- Recommended build-module header style:
module my_pkg::build as Builder;(preferred;Builderis in the std prelude)- or
module my_pkg::build as std::interfaces::Builder;(fully qualified)
Drop semantics#
std::interfaces::Drop is recognized by the compiler as the standard way for a
type to release resources it owns (file descriptors, heap allocations, OS
handles, etc.). A type is considered “droppable” when it provides a method with
this surface signature:
impl T as Drop {
public fn drop (mut self: &T) -> void { ... }
}
Automatic invocation :
- Scope exit: when a
structvalue binding goes out of scope (including via fallthrough,break, andcontinue), the compiler callsdropbefore the storage is discarded. - Return: on
return, the compiler drops all in-scope droppable bindings except any value moved into the return result (for examplereturn value;andreturn Some(value);treatvalueas moved in the Supported forms). - Overwrite: when a
structvalue binding is overwritten via assignment, the compiler callsdropon the old value before copying in the new value. - Heap last-release: for compiler-managed
newallocations (&Twith RC), the compiler callsdropbefore freeing the backing allocation when the refcount reaches zero.
Notes and limitations (Supported forms):
dropis resolved statically (no dynamic dispatch).dropshould invalidate the value so calling it multiple times is safe.- The language does not yet implement a general move/ownership model; do not
rely on copying
Droptypes to be safe until move/copy semantics are specified and enforced. - See memory model for the current
new+ RC rules and how cleanup is performed.
Example (Conformance)#
struct Counter {
value: i64,
}
impl Counter as Len {
public fn len (self: &Counter) -> i64 {
return self.value;
}
}
Example (Serialize(string) in the stdlib)#
import c_owned from "std/ffi/c_owned";
import std::path;
import std::strings;
import std::url;
import std::runtime::mem;
fn main () -> int {
let owned_r = std::strings::String.from_string("hello");
let mut owned = match (owned_r) {
Ok(v) => v,
Err(_) => std::strings::String.empty(),
};
let s0: string = owned as string;
let pb_r = std::path::PathBuf.from_string("/tmp/demo");
let mut pb = match (pb_r) {
Ok(v) => v,
Err(_) => std::path::PathBuf{ ptr: 0, cap: 0, len: 0 },
};
let s1: string = pb as string;
let params_r = std::url::URLSearchParams.from_string("?a=b%20c");
let mut params = match (params_r) {
Ok(v) => v,
Err(_) => std::url::URLSearchParams.empty(),
};
let s2: string = params as string; // "a=b+c"
let p: u64 = std::runtime::mem::alloc(3);
if p == 0 {
owned.drop();
pb.drop();
params.drop();
return 4;
}
std::runtime::mem::store_u8(p, 0, 104);
std::runtime::mem::store_u8(p, 1, 105);
std::runtime::mem::store_u8(p, 2, 0);
let free_fn: c_owned::CFreeFn = fn (ptr: u64) {
std::runtime::mem::free(ptr);
};
let mut c_str = c_owned::OwnedCStr.from_ptr(p, free_fn);
let s3: string = c_str as string;
if s0 != "hello" { return 1; }
if s1 != "/tmp/demo" { return 2; }
if s2 != "a=b+c" { return 3; }
if s3 != "hi" { return 4; }
owned.drop();
pb.drop();
params.drop();
c_str.drop();
return 0;
}
Example (TrySerialize for owned text output)#
import std::semver;
import std::uuid;
fn main () -> int {
match (std::semver::Version.parse("1.2.3-alpha+build.5")) {
Ok(v) => {
match (v.try_serialize()) {
Ok(mut s) => {
if (s as string) != "1.2.3-alpha+build.5" {
s.drop();
return 1;
}
s.drop();
},
Err(_) => { return 2; },
}
},
Err(_) => { return 3; },
}
match (std::uuid::UUID.parse("550e8400-e29b-41d4-a716-446655440000")) {
Ok(id) => {
match (id.try_serialize()) {
Ok(mut s) => {
if (s as string) != "550e8400-e29b-41d4-a716-446655440000" {
s.drop();
return 4;
}
s.drop();
return 0;
},
Err(_) => { return 5; },
}
},
Err(_) => { return 6; },
}
}
Example (Parse in the stdlib)#
import std::path;
import std::semver;
import std::url;
import std::uuid;
fn main () -> int {
match (std::semver::Version.parse("1.2.3")) {
Ok(v) => {
if v.major != 1 { return 1; }
},
Err(_) => { return 2; },
}
match (std::path::PathBuf.parse("/tmp/demo")) {
Ok(mut pb) => {
let s: string = pb as string;
if s != "/tmp/demo" {
pb.drop();
return 3;
}
pb.drop();
},
Err(_) => { return 4; },
}
match (std::url::URL.parse("https://example.com?a=b")) {
Ok(mut u) => {
u.drop();
},
Err(_) => { return 5; },
}
match (std::uuid::UUID.parse("550e8400-e29b-41d4-a716-446655440000")) {
Ok(_) => { return 0; },
Err(_) => { return 6; },
}
}
Source repository · Edit this page · View Markdown