Standard library / std::strings

std::strings

A small but growing subset is implemented in std/strings.slk to support early toolchain bring-up; the rest of this document describes the intended long-term API.

This module provides string utilities and abstractions built on top of the core string type (UTF-8 bytes) and the Buffer(T) intrinsic.

See also:

  • literals string (string semantics: UTF-8 bytes)
  • ext (ABI/external-call representation and null-termination rule)
  • buffers (Buffer(T) as the low-level backing store)
  • conventions (UTF-8, allocation, and error conventions)

Exported API#

The following functions exist today in std/strings.slk and are available to import:

module std::strings;

export fn eq (a: string, b: string) -> bool;
export fn ne (a: string, b: string) -> bool;
export fn lt (a: string, b: string) -> bool;
export fn le (a: string, b: string) -> bool;
export fn gt (a: string, b: string) -> bool;
export fn ge (a: string, b: string) -> bool;

export fn is_empty (s: string) -> bool;
export fn or_empty (s: string?) -> string;
export fn starts_with (s: string, prefix: string) -> bool;
export fn ends_with (s: string, suffix: string) -> bool;

export fn trim (s: string) -> string;
export fn trim_start (s: string) -> string;
export fn trim_end (s: string) -> string;

export fn pad_left (s: string, min_len: i64, pad: string) -> std::result::Result(String, std::memory::OutOfMemory);
export fn pad_right (s: string, min_len: i64, pad: string) -> std::result::Result(String, std::memory::OutOfMemory);
export fn pad_center (s: string, min_len: i64, pad: string) -> std::result::Result(String, std::memory::OutOfMemory);

Notes:

  • These are simple wrappers over the language’s built-in string comparisons and optional-coalesce operator (??), chosen because they are implementable in Silk currently.
  • starts_with and ends_with compare the UTF-8 byte sequences carried by string. They do not normalize Unicode or compare grapheme clusters. An empty prefix or suffix matches every string.
  • This surface will grow alongside language/runtime features required for richer string operations (slicing, iteration, allocation, etc.).

In addition, a low-level StringBuilder type exists today for incremental byte construction:

struct StringBuilder {
  ptr: u64,
  cap: i64,
  len: i64,
}

impl StringBuilder {
  public fn init (cap: i64) -> std::result::Result(StringBuilder, std::memory::AllocFailed);
  public fn empty () -> StringBuilder;
  public fn push_u8 (mut self: &StringBuilder, value: u8) -> std::memory::OutOfMemory?;
  public fn push_char (mut self: &StringBuilder, value: char) -> std::memory::OutOfMemory?;
  public fn pop_u8 (mut self: &StringBuilder) -> u8?;
  public fn get_u8 (self: &StringBuilder, index: i64) -> u8;
  public fn set_u8 (mut self: &StringBuilder, index: i64, value: u8) -> void;
  public fn into_string (mut self: &StringBuilder) -> std::result::Result(String, std::memory::OutOfMemory);
}

impl StringBuilder as std::interfaces::ReserveAdditional {
  public fn reserve_additional (mut self: &StringBuilder, additional: i64) -> std::memory::OutOfMemory?;
}

Notes:

  • StringBuilder builds raw bytes. It can be converted into an owned String via into_string; the resulting String can then yield a borrowed string view via String.as_string().
  • StringBuilder.push_char(value) encodes one Unicode scalar value into UTF-8 bytes and appends those bytes. Use push_u8 only when the caller already has encoded bytes.
  • StringBuilder allocation failure is recoverable:
  • init(cap) returns Err(AllocFailed) when the initial allocation fails (or when cap is invalid),
  • empty() exists for infallible construction,
  • growth paths (push_u8, reserve_additional, into_string) return std::memory::OutOfMemory? / Result(...) and leave the builder unchanged on failure (including internal size arithmetic overflow).
  • StringBuilder conforms to common std::interfaces protocols:
  • Len, Capacity, IsEmpty, Clear, ReserveAdditional, WriteU8, and Drop.

An owned String type exists today for dynamically produced strings:

struct String {
  ptr: u64,
  cap: i64,
  len: i64,
}

impl String {
  public fn empty () -> String;
  public fn from_string (s: string) -> std::result::Result(String, std::memory::OutOfMemory);
  public fn from_buffer_u8 (mut v: &std::buffer::BufferU8) -> std::result::Result(String, std::memory::OutOfMemory);
  public fn from_chars (chars: std::arrays::Slice(char)) -> std::result::Result(String, std::memory::OutOfMemory);
  public fn as_string (self: &String) -> string;

  public fn push_u8 (mut self: &String, value: u8) -> std::memory::OutOfMemory?;
  public fn push_string (mut self: &String, s: string) -> std::memory::OutOfMemory?;
  public fn push_repeat_u8 (mut self: &String, byte: u8, count: i64) -> std::memory::OutOfMemory?;

  public fn trim (mut self: &String) -> void;
  public fn trim_start (mut self: &String) -> void;
  public fn trim_end (mut self: &String) -> void;

  public fn pad_left (mut self: &String, min_len: i64, pad: string) -> std::memory::OutOfMemory?;
  public fn pad_right (mut self: &String, min_len: i64, pad: string) -> std::memory::OutOfMemory?;
  public fn pad_center (mut self: &String, min_len: i64, pad: string) -> std::memory::OutOfMemory?;
}

Notes:

  • String uses the zero-capacity empty state { ptr: 0, cap: 0, len: 0 } when no allocation is needed. Allocated states maintain a trailing NUL terminator and satisfy std::strings::string_storage_well_formed, so the borrowed string view from as_string() remains safe for C APIs that expect const char *.
  • The byte-oriented constructors and mutators (from_string, from_buffer_u8, push_u8, push_string, and push_repeat_u8) copy or append bytes and do not validate UTF‑8.
  • String.from_chars(chars) encodes a std::arrays::Slice(char) into UTF-8 bytes before constructing the owned String. It is the canonical owned conversion from scalar slices.
  • String.as_string() yields a non-owning view into the String allocation; callers must not use the returned string after the String is dropped.
  • String also implements std::interfaces::{Len,Capacity,IsEmpty,Clear,ReserveAdditional,WriteU8,Serialize(string),TrySerialize(std::memory::OutOfMemory),Parse(std::memory::OutOfMemory),Drop}. In practice this means let s: string = owned as string; is the standard, allocation-free way to borrow an owned String as a plain string.
  • Because String implements Serialize(string), the compiler also accepts it directly in ordinary borrowed string sites such as string parameters and let x: string = owned.
  • Returning owned directly from fn ... -> string remains lifetime-sensitive and is rejected by the compiler; prefer returning String itself or spelling the borrow explicitly with .as_string() when that is the intended local contract.
  • owned.try_serialize() is the canonical fallible owned-string rendering path; it clones the current contents into a new std::strings::String.
  • String.parse(s) is a standardized alias for String.from_string(s), which makes generic fallible string-construction code read consistently across the stdlib.
  • std::strings also owns the reusable verification vocabulary for this representation: downstream verified code that wants to talk about owned string/path/search-params storage should import std::strings::string_storage_well_formed(...), not std::formal.

Example:

import std::strings;

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 view: string = owned as string;
  if view != "hello" {
    owned.drop();
    return 1;
  }

  match (std::strings::String.parse("world")) {
    Ok(mut parsed) => {
      if (parsed as string) != "world" {
        parsed.drop();
        owned.drop();
        return 2;
      }
      parsed.drop();
    },
    Err(_) => {
      owned.drop();
      return 3;
    },
  }

  owned.drop();
  return 0;
}

Example: converting scalar values requires UTF-8 encoding, not a pointer cast:

import std::arrays;
import std::strings;

fn main () -> int {
  let chars: char[5] = ['h', 'e', 'l', 'l', 'o'];
  let slice: std::arrays::Slice(char) = std::arrays::Slice(char).init(chars as u64, 5);

  let r = std::strings::String.from_chars(slice);
  if r.is_err() {
    return 1;
  }

  let mut owned: std::strings::String = match (r) {
    Ok(v) => v,
    Err(_) => std::strings::String.empty(),
  };

  if owned.as_string() != "hello" {
    owned.drop();
    return 2;
  }

  owned.drop();
  return 0;
}

Do not cast a char[] backing pointer to string; char is a Unicode scalar value, and its memory representation is not UTF-8 bytes. Raw pointer casts are only valid for buffers that already contain encoded bytes.

Scope#

std::strings is responsible for:

  • Construction, slicing, and concatenation.
  • UTF-8-aware utilities (iteration by char, validation when constructing from raw bytes).
  • Interoperability with FFI (SilkString, C-string compatibility).

Non-goals (initially):

  • Locale-aware collation and normalization (future work).
  • Full Unicode grapheme segmentation (future work).

Core Types#

The language provides a built-in string type (an immutable UTF-8 byte sequence). The stdlib adds:

  • Str — a non-owning view over UTF-8 bytes (useful when the caller wants an explicit view type rather than string).
  • String — an owning, growable UTF-8 string backed by Buffer(u8) plus a length (a dynamic array of bytes that maintains UTF-8 validity).
  • StringBuilder — a convenience for incremental construction; typically a thin wrapper around String or a packed byte buffer (for example std::buffer::BufferU8).

Key invariants:

  • String must always contain valid UTF-8.
  • When converting a String to a string for FFI, the backing storage must be null-terminated (with the trailing \0 byte not counted in .len), matching the external-call contract in ext.

API Sketch (Illustrative)#

These signatures are illustrative and will be refined alongside the language features required to implement them (references, generics, enums/results, etc.).

module std::strings;

export struct String {
  // Invariant: `buf[0..len]` is valid UTF-8; `buf[len] == 0` for C interop.
  buf: Buffer(u8),
  len: int,
}

export fn empty () -> String;
export fn from_string (alloc: std::memory::Allocator, s: string) -> String;
export fn from_utf8 (alloc: std::memory::Allocator, bytes: std::arrays::Slice(u8)) -> Result(String, Utf8Error);

export fn as_string (s: &String) -> string;
export fn len_bytes (s: string) -> int;
export fn is_empty (s: string) -> bool;

export fn starts_with (s: string, prefix: string) -> bool;
export fn ends_with (s: string, suffix: string) -> bool;
export fn find (s: string, needle: string) -> int?;

export fn concat (alloc: std::memory::Allocator, a: string, b: string) -> String;

FFI Interop#

string values crossing the C ABI use SilkString { ptr, len } as documented in ext and abi libsilk.

std::strings should provide helpers for common interop patterns:

  • Passing string to C APIs that expect const char * (use .ptr; Silk’s runtime representation guarantees a trailing NUL).
  • Producing an owned, NUL-terminated string for FFI calls that require the backing storage to outlive the call (e.g. when C stores the pointer).

Considerations#

  • split, replace, join.
  • UTF-8 scalar iteration (chars()), case mapping, and normalization.
  • Formatting integration (shared with std::io).

Implementation must respect the ownership and lifetime rules from ext and buffers.

Source repository · Edit this page · View Markdown