std/core

The bottom of the standard library: Option, Result, Error, the operator and conversion traits, and the control structures that are functions.

The generated reference of std/core has every public construct with its signature, its doc comment and its examples.

std/core is what every other package builds on: the types the language itself refers to - Option behind Value?, Result behind ?, the three ranges behind a..b, Array as the inline storage a list literal adapts to, Buffer as the heap storage everything that grows stands on - the traits the operators and the conversions go through, and the control flow that is an ordinary function. Everything in it is re-exported by the prelude, so a file rarely imports it by name - except the three target types, which a file that branches on where it runs imports on purpose, and Plain, which a bound that lends values to a worker names on purpose.

A data structure is not in here: List, Map and Set are written on top of a storage primitive and live in std/collections. Nothing in std/core knows about text or numbers - with the one exception that String is unavoidable in a signature (Show.show, panic). That import points at std/text, which is why the packages of the standard library are cyclic, and cycles between modules are allowed because nothing runs when a module is imported.

Import#

use Option, Result, Error from "std/core"
use Option.Some, Option.None, Result.Ok, Result.Fail from "std/core"

Every name below is already in scope through the prelude, so an import is only needed in a file whose project names another prelude, or to make the dependency explicit.

Declarations#

Option#

type Option<Value> with OrElse<Value> {
  case Some(value: Value)
  case None
  fn isSome(): Bool
  fn isNone(): Bool
  fn map<Output>(transform: Transform<Value, Output>): Output?
  fn flatMap<Output>(transform: Transform<Value, Output?>): Output?
  fn filter(predicate: Predicate<Value>): Value?
  fn forEach(action: Action<Value>)
  fn toList(): List<Value>
  fn orElse(fallback: lazy Value): Value
  fn okOr<Failure>(error: lazy Failure): Result<Value, Failure>
  fn expect(message: String): Value
}
extend<Value: Show> Option<Value> with Show
extend<Value, Target: From<Iterate<Value>>> Option<Target> with From<Iterate<Value?>>

A value that may be absent. Value? is sugar for Option<Value>. map, flatMap, filter and forEach mean the same as everywhere else and run immediately, because an Option is a value and not a pipeline. orElse is OrElse, the trait ?? goes through, okOr turns it into a Result, toList() brings it into the world of pipelines, and expect(message) panics where absence is a bug.

Result#

type Result<Value, Failure> with OrElse<Value> {
  case Ok(value: Value)
  case Fail(error: Failure)
  fn isOk(): Bool
  fn isError(): Bool
  fn map<Output>(transform: Transform<Value, Output>): Result<Output, Failure>
  fn mapError<OtherFailure>(transform: (error: Failure) => OtherFailure): Result<Value, OtherFailure>
  fn flatMap<Output>(transform: Transform<Value, Result<Output, Failure>>): Result<Output, Failure>
  fn forEach(action: Action<Value>)
  fn toList(): List<Value>
  fn ok(): Value?
  fn orElse(fallback: lazy Value): Value
  fn expect(message: String): Value where Failure: Show
}
extend<Value: Show, Failure: Show> Result<Value, Failure> with Show
extend< Value, Failure, Target: From<Iterate<Value>>, > Result<Target, Failure> with From<Iterate<Result<Value, Failure>>>

The result of an operation that can fail. The postfix ? unwraps Ok or returns the Fail from the surrounding function, converting the error type through From. map, flatMap and forEach work on the Ok side, mapError on the Fail side. isOk, isError, ok, orElse, toList and expect are the rest. See Result for the rules.

Error#

trait Error with Show {
  fn cause(): Error?
}

"Some error, hand it up." Every error type that carries this trait fits into Result<Value, Error>, which lets an application pass a failure through layers that have nothing to say about it. Precise error types stay the norm for a library, because a caller can only match on what a signature names. cause() is the chain, and its default is None, so a type that has nothing to add implements the trait by writing with Error and nothing else.

Void, Never and panic#

Void is the type with exactly one value, and that value is the keyword literal void. A function without a result type answers Void, a block that ends in a statement has the value void, and Ok(void) passes it on. Never is the type of an expression that does not return, which is why panic "..." fits into any expression. panic prints panic: <message> and the site to standard error and exits with 101; nothing else runs on the way out.

Bool#

Bool with Equals, Hash, Show and Encode. &&, || and ! are built in, they short-circuit, and they cannot be overloaded, because a trait method would evaluate its argument.

Equals, Compare, Ordering, Hash, combineHashes#

Equals has equals and is ==. Compare has compare, requires Equals, and is <, <=, > and >=; its default members include min and max. Ordering is what compare answers. Hash has hash, and combineHashes is what a hand-written hash folds with. Float32 and Float64 are deliberately not Hash, so a float can never be a Map key and the nan key does not exist.

From, Into, TryFrom, TryInto, Show, LiteralParseError#

Conversions follow From and Into: implementing From provides Into for free through a blanket implementation. TryFrom is the fallible form and provides TryInto the same way, and text is a source like any other (Int.tryFrom("42")). One direction of each pair is the one to implement, and an extend that writes Into or TryInto by hand is told which From or TryFrom to write instead. Every type has From<Self>, and that conversion is the value itself. Show has show and is what string interpolation calls; showNested is what a value inside another value uses, and only String and Char override it. LiteralParseError is what the generated TryFrom<String, ...> of a literal type answers.

RawValue#

trait RawValue {
  fn rawValue(): Int
  static fn fromRawValue(value: Int): Self?
}

The number a case stands for and back, generated for every type whose cases all carry one (case Read = 1, Cases that stand for numbers): rawValue() answers the number of the case, fromRawValue the case of a number or None. It is in the prelude, so it can be a bound without an import, which is what Flags<Case: RawValue> of std/collections is.

The operator traits#

Add, Subtract, Multiply, Divide, Remainder, Power, Negate, OrElse, Index, MutableIndex, Slice, MutableSlice. Each one has a type parameter list with defaults, which is why with Add means Add<Self, Self>. a ** b is Power.power, whose first parameter is the Exponent (an integer is raised by an Int, a float by a float or by an Int). a[i] is Index.at, a[i] = v is MutableIndex.set, a[from..to] is Slice.slice, a[from..to] = v is MutableSlice.replace, and a ?? b is OrElse.orElse, whose fallback is lazy so that it is only evaluated where there is nothing to give back. MutableIndex requires keys() besides set: the keys a for var item in items visits, in its order - the indices 0..length() of a List and an Array, the keys of a Map in insertion order (Changing elements in place).

Range, RangeFrom, RangeTo, Bounds#

native type Range<Value> with Equals, Hash {
  start: Value
  end: Value
  inclusive: Bool = false
}
extend<Value: Compare> Range<Value> with Bounds<Value>
extend Range<Int> with Iterate<Int>, Length
extend<Value: Show> Range<Value> with Show
native type RangeFrom<Value> with Equals, Hash {
  start: Value
}
extend<Value: Compare> RangeFrom<Value> with Bounds<Value>
extend RangeFrom<Int> with Iterate<Int>
extend<Value: Show> RangeFrom<Value> with Show
native type RangeTo<Value> with Equals, Hash {
  end: Value
  inclusive: Bool = false
}
extend<Value: Compare> RangeTo<Value> with Bounds<Value>
extend<Value: Show> RangeTo<Value> with Show
trait Bounds<Value: Compare> {
  fn lowest(): Value?
  fn highest(): Value?
  fn includesHighest(): Bool
  fn contains(value: Value): Bool
}

Which ends a range has is its type, chosen by the syntax: a..b and a..=b are a Range, a.. a RangeFrom, ..b and ..=b a RangeTo. Nothing is optional, so for index in ..10 and (0..).length() are compile errors where they are written: Range<Int> is Iterate<Int> and Length, RangeFrom<Int> is Iterate<Int> and endless, RangeTo<Int> is neither. Bounds<Value> is what all three are and what Slice.slice takes, so every spelling works in brackets.

Array#

native type Array<Item, const Size: Int> with Iterate<Item>, Length, MutableIndex<Int, Item> {
  static fn filled(value: Item): Self
  fn keys(): Iterate<Int>
  static fn generated(produce: (index: Int) => Item): Self
  static fn from(items: Iterate<Item>): Self?
  fn length(): Int
  fn get(index: Int): Item?
  fn at(index: Int): Item
  fn iterate(): Iterator<Item>
  var fn set(index: Int, value: Item)
  var fn fill(value: Item)
  fn mapped<Output>(transform: Transform<Item, Output>): Array<Output, Size>
}
extend<Item: Equals, const Size: Int> Array<Item, Size> with Equals
extend<Item: Show, const Size: Int> Array<Item, Size> with Show
extend<Item: Hash, const Size: Int> Array<Item, Size> with Hash

The inline storage primitive: a fixed number of items, and the number is part of the type (Array<Float, 16>). No storage on the heap, no reference count, and copying it copies its items. Size is a const parameter - a literal, a named const or another const parameter - and there is no arithmetic over one, so an out-of-bounds index the compiler can work out at the call site is a compile error rather than a panic.

Every way to build one writes its size down. A list literal against an expected Array type goes straight into the inline slots and its items are counted against Size; a ... inside such a literal is allowed when what it spreads is itself an Array, and the sizes add up. filled and generated take Size from the expected type, and from counts at run time and answers Array<Item, Size>?. It is Equals, Hash and Show wherever Item is, and the first two by index and therefore order-dependent. See Arrays and const parameters.

Buffer#

native type Buffer<Item> with Iterate<Item>, Length, MutableIndex<Int, Item>, MutableSlice, From<Iterate<Item>> {
  static fn from(items: Iterate<Item>): Self
  native static fn withCapacity(capacity: Int): Self
  native static fn holdsClose(): Bool
  native fn length(): Int
  native fn get(index: Int): Item?
  fn at(index: Int): Item
  native var fn set(index: Int, value: Item)
  var fn update(index: Int, change: (var element: Item) => Void)
  native var fn append(value: Item)
  native var fn insert(index: Int, value: Item)
  native var fn removeAt(index: Int): Item?
  var fn swapRemove(index: Int): Item?
  fn slice(range: Bounds<Int>): Self
  var fn replace(range: Bounds<Int>, values: Self)
  native var fn reverse()
  native var fn clear()
  fn iterate(): Iterator<Item>
  fn keys(): Iterate<Int>
}

The growable heap storage primitive: a sequence that is written in place while its storage has one owner. buffer[i] = x, set, append, insert, removeAt, swapRemove, replace, reverse and clear change the storage itself; they copy it once (copy on write) only when the Buffer value has been copied - const other = buffer, passed or captured by value - and is then written. Nothing stronger is promised, so keep a big buffer in one var owner and pass it as a var parameter.

swapRemove(index) answers the item at index and moves the last item into its slot: O(1), the order is not kept, and None (with no change) answers an index out of range. slice is a copy that owns its storage, unlike the shared slice of a List, so writing the original never turns into a second copy. Buffer implements only what std/core owns (Iterate, Length, MutableIndex, MutableSlice, Equals, Show, Hash); the List vocabulary belongs to std/collections.

Shared and isSame#

Shared<Value> is a box that puts a value in a place several owners can hold - the ad hoc version of a shared type. isSame(first, second) compares identity and works on a shared object only: on a value the answer would expose whether the implementation shares storage, so the compiler rejects it.

Plain#

A type whose values hold nothing reference counted, anywhere inside them. The compiler grants it from the layout of the type, the way it generates Show, and nothing else can: a with Plain or an extend ... with Plain is an error. It is what lets code read a value through a borrowed pointer without touching a count, which is what a worker that is lent a part of the caller's memory needs. It is not in the prelude.

trait Plain

Int, Float, Bool, Char, the sized numbers and Array<Float, 16> are Plain, and so is every record, variant and tuple whose fields all are. String, List, Map, Decimal and everything that holds one are not, and neither is a shared type, a trait-typed value, a function value or a type that contains itself: each of those is held in a counted block. A type parameter is Plain where its bound says so.

use Plain from "std/core"

type Point {
  x: Float
  y: Float
}

fn first<Item: Plain>(items: List<Item>): Item {
  items[0]
}

print first([1, 2, 3])
print first([Point(x: 1.0, y: 2.0)]).x

A refusal names the field or the case that holds the counted type, down to that type:

Does not compile
use Plain from "std/core"

type User {
  name: String
  age: Int
}

fn first<Item: Plain>(items: List<Item>): Item {
  items[0]
}

print first([User(name: "Ada", age: 36)]).age
// error: `User` does not implement `Plain`

The message goes on with "its field name: String is not Plain: User.name -> String" and "String is reference counted".

do, unless, retry and Close#

Control structures that are ordinary functions, because only a construct that binds names or jumps is built in, and the destructor trait.

fn do<Value>(body: () => Value): Value
fn unless(condition: Bool, body: () => Void)
fn retry<Value, Failure>(times: Int, action: () => Result<Value, Failure>): Result<Value, Failure>
shared trait Close {
  var fn close()
}

Close is the destructor: only a shared type implements it, and no program calls close() - the last release of the object runs it. using name = value is the binding that pins that release to the end of a block; it is part of the grammar, not a function of std/core (see Destructors, which also says what is built of it today).

OperatingSystem, Architecture and ByteOrder#

What the program is compiled for, each with one static current: a compile-time constant that torb build --target sets, the machine torb runs on by default. A match on one of them keeps only the arm the value selects, while every arm is type checked on every machine (Compile-time branches). They are not in the prelude: the import is the statement that a file branches on its target.

type OperatingSystem with Show, Equals, Hash {
  case Windows
  case Linux
  case MacOs
  case FreeBsd
  case Browser
  static current: Self = targetOperatingSystem()
  fn isPosix(): Bool
  fn show(): String
}
type Architecture with Show, Equals, Hash {
  case X64
  case Arm64
  case Wasm64
  static current: Self = targetArchitecture()
  fn show(): String
}
type ByteOrder with Show, Equals, Hash {
  case LittleEndian
  case BigEndian
  static current: Self = targetByteOrder()
}
use OperatingSystem from "std/core"

fn separator(): String {
  match OperatingSystem.current {
    .Windows => ";"
    .Linux | .MacOs | .FreeBsd | .Browser => ":"
  }
}

print(OperatingSystem.Linux.isPosix() && separator().byteLength() == 1)
// prints true

show() answers the name as the vendor writes it (macOS, FreeBSD), and isPosix() is the question for a branch that should include an operating system added later. Browser on Wasm64 is the target of the playground, whose torb runs as WebAssembly in a web page (the playground); isPosix() is false there.

Predicate, Action and Transform#

The three closure shapes a signature takes most, as aliases: a question about one value, an effect on one value, and the conversion of one value into another. An alias is transparent, so any closure of the shape fits (see Predicate, Action and Transform).

type Predicate<Value> = (value: Value) => Bool
type Action<Value> = (value: Value) => Void
type Transform<Input, Output> = (value: Input) => Output
Esc