The source of every builtin member, written in the language itself - all 2191 functions and methods the Types and Interfaces pages list, and every interface implementation their types claim. Each member there links to its code here.
global method name for Type and global function name for Type definitions - global, since code in every folder can reach them. A method takes self as its first argument and is called on a value; a function has no self and is called on the type. There's no implicit receiver: a field is always self.field, and a sibling self.name(). implement Interface for Type: every function that fulfils one of the interface's members is written in that block, never as a global one. A method for an interface itself - like for Iterable<Type> - is one every implementor gets for free. Self is the type a function is for, and the generics after it are the type's, then the function's own. The field lines under a type are its hidden layout, and local members are private helpers - neither can be reached from outside. IntegerTypes (or FloatTypes, TextTypes, ...) is one copy per type in the family: Self is that type, and an intrinsic whose name says Self is that type's own. yield is a generator - calling it returns an Iterator over what it yields. #builtin for the machine and runtime library - memory, Unicode tables, the regex engine, and the intrinsics; #comptime for what the compiler knows about types and functions; #runtime for the running program's own values. Each member lists the ones it uses. Numbers bottom out in intrinsics, collections in Pointer. #helpers/// the number type families a function is written once for - one copy per type in the family
local const IntegerTypes := U8 | U16 | U32 | U64 | U128 | U256 | S8 | S16 | S32 | S64 | S128 | S256local const FloatTypes := F16 | F32 | F64/// Part below is F32, F64 respectively
local const ComplexTypes := C32 | C64/// Part below is F32, F64 respectively
local const QuaternionTypes := Q32 | Q64/// the same members, differing only in encoding
local const TextTypes := UTF8 | UTF16 | UTF32 | ASCII uses #builtin.forever
/// the endless count 0, 1, 2, ... The only loop is For, so an open-ended loop is a For over this with a
/// while / until modifier, or a Return / Break inside
local function forever(): Iterator<U64> {
return #builtin.forever()
}/// FNV-1a over bytes - what every builtin Hashable is built on, starting from &hashSeed
local const hashPrime: U64 := 0x100000001b3local function hashBytes(bytes: Bytes, seed: U64 := &hashSeed): U64 {
let hash := seed
for byte in bytes {
set hash := (hash bxor (byte as U64)).wrappingMultiply(hashPrime)
}
return hash
}/// folds one more hash into a running one - for hashing several parts (fields, elements) in order
local function hashCombine(hash: U64, next: U64): U64 {
return hashBytes(next.toBytes(), hash)
}/// hands error to &panicHandler - by default printing it and its trace() to &stderr and ending the program.
/// What Optional.expect and a failed builtin allocation come down to
local function abort(error: Error): Never {
return &panicHandler.handle(error)
}/// room for count Ts - builtin collections don't thread OutOfMemoryError through every signature, running out
/// of memory while growing one aborts instead
local function allocate<T>(count: U64): Pointer<T> {
match Pointer<T>.allocateMany(count) {
case Error(e) then { return abort(e) }
case pointer then { return pointer }
}
}/// the first index in a range and the one past its last, whichever way it was written (a..<b, a..=b)
local function bounds(range: Range<U64>): (U64, U64) {
return (range.start(), range.isInclusive() ? range.end() + 1 : range.end())
}/// the digit character for 0 ... 35 - 0-9 then a-z
local function digitChar(value: U8): Codepoint {
let code := value < 10 ? 48 + value : 87 + value
return Codepoint.fromU32(code as U32) !? '?'
}/// value as JSON - with an indent, each object's and array's members on lines of their own, depth deep
local function writeJson for Json(value: Unknown, indent: String, depth: String): String {
let inner := depth ++ indent
let (open, between, close) := indent == "" ? ("", ",", "") : ("\n" ++ inner, ",\n" ++ inner, "\n" ++ depth)
match value {
case undefined then {
return "null"
}
case String as text then {
let escaped := text.codepoints().map(λ(c) c == '"' ? "\\\"" : c == '\\' ? "\\\\" : c == '\n' ? "\\n" : c == '\r' ? "\\r" : c == '\t' ? "\\t" : c.value() < 32 ? "\\u" ++ c.value().toString(16).padStart(4, '0') : c.toString())
return "\"" ++ escaped.join("") ++ "\""
}
case Boolean as truth then {
return truth.toString()
}
case F64 as number if not number.isFinite() then {
return "null"
}
case Array<Unknown> as items then {
return items.isEmpty() ? "[]" : "[" ++ open ++ items.map(λ(item) writeJson(item, indent, inner)).join(between) ++ close ++ "]"
}
case Map<Unknown, Unknown> as members then {
let written := members.entries().map(λ((name, member)) writeJson(name.toString(), "", "") ++ (indent == "" ? ":" : ": ") ++ writeJson(member, indent, inner))
return members.isEmpty() ? "{}" : "{" ++ open ++ written.join(between) ++ close ++ "}"
}
case Struct as s then {
return writeJson(s.fields(), indent, depth)
}
case _ if value.typeOf().implements(Numeric) then {
return value.toString()
}
case _ then {
return writeJson(value.toString(), indent, depth)
}
}
}local function of for DocumentReader(text: String): DocumentReader {
return DocumentReader{text := text.codepoints(), at := 0}
}local method peek for DocumentReader(self: DocumentReader, ahead: U64 := 0): Codepoint? {
return self.text[self.at + ahead]
}local method atEnd for DocumentReader(self: DocumentReader): Boolean {
return self.at >= self.text.size()
}local method skipSpace for DocumentReader(self: DocumentReader): Undefined {
for _ in forever() while self.peek() == ' ' | '\n' | '\r' | '\t' {
set self.at += 1
}
}/// word next - read past it when it is
local method take for DocumentReader(self: DocumentReader, word: String): Boolean {
if word.codepoints().enumerate().all(λ((i, c)) self.peek(i) == c) then {
set self.at += word.size()
return true
}
return false
}/// what went wrong, and the line and column it's at
local method failure for DocumentReader(self: DocumentReader, format: String, what: String): ParseError {
let before := self.text.take(self.at)
let line := before.count(λ(c) c == '\n') + 1
let column := self.at - (before.lastIndexOf('\n').map(λ(i) i + 1) ?? 0) + 1
return ParseError.new("not " ++ format ++ " - " ++ what ++ ", at line " ++ line.toString() ++ ", column " ++ column.toString())
}local method jsonValue for DocumentReader(self: DocumentReader): ParseError ! Unknown {
self.skipSpace()
match self.peek() {
case '{' then {
return self.jsonObject()!!
}
case '[' then {
return self.jsonArray()!!
}
case '"' then {
return self.jsonString()!!
}
case _ then {
for (word, value) in [("true", true as Unknown), ("false", false), ("null", undefined)] if self.take(word) {
return value
}
let start := self.at
for _ in forever() while "-+.eE0123456789".contains(self.peek() ?? ' ') {
set self.at += 1
}
if self.at == start then {
error self.failure("JSON", self.atEnd() ? "the document ends early" : "'" ++ (self.peek() ?? ' ').toString() ++ "' starts no value")
}
return F64.parse(String.fromCodepoints(self.text.slice(start..<self.at)!!)) !? (error self.failure("JSON", "not a number"))
}
}
}local method jsonObject for DocumentReader(self: DocumentReader): ParseError ! Map<String, Unknown> {
set self.at += 1
let members: Map<String, Unknown> := Map.of()
self.skipSpace()
if self.take("}") then {
return members
}
for _ in forever() {
self.skipSpace()
if self.peek() != '"' then {
error self.failure("JSON", "a member's name is a string")
}
let name := self.jsonString()!!
self.skipSpace()
if not self.take(":") then {
error self.failure("JSON", "expected ':'")
}
members.set(name, self.jsonValue()!!)
self.skipSpace()
if self.take("}") then {
return members
}
if not self.take(",") then {
error self.failure("JSON", "expected ',' or '}'")
}
}
}local method jsonArray for DocumentReader(self: DocumentReader): ParseError ! Array<Unknown> {
set self.at += 1
let items: Array<Unknown> := []
self.skipSpace()
if self.take("]") then {
return items
}
for _ in forever() {
items.push(self.jsonValue()!!)
self.skipSpace()
if self.take("]") then {
return items
}
if not self.take(",") then {
error self.failure("JSON", "expected ',' or ']'")
}
}
}/// count hex digits' value
local method hex for DocumentReader(self: DocumentReader, count: U64): ParseError ! U32 {
let value: U32 := 0
for _ in 0..<count {
let digit := self.peek()?.digitValue(16) ?! self.failure("JSON", "an escape's hex digits")
set value := value * 16 + digit as U32
set self.at += 1
}
return value
}local method jsonString for DocumentReader(self: DocumentReader): ParseError ! String {
set self.at += 1
let out: Array<Codepoint> := []
for _ in forever() {
let c := self.peek() ?! self.failure("JSON", "a string that never ends")
set self.at += 1
if c == '"' then {
return String.fromCodepoints(out)
}
if c != '\\' then {
out.push(c)
continue _
}
let escaped := self.peek() ?! self.failure("JSON", "a string that never ends")
set self.at += 1
if escaped == 'u' then {
let code := self.hex(4)!!
/// a pair of surrogates is one code point
if code >= 0xD800 and code <= 0xDBFF and self.take("\\u") then {
set code := 0x10000 + (code - 0xD800) * 0x400 + (self.hex(4)!! - 0xDC00)
}
out.push(Codepoint.fromU32(code)!!)
} else {
let index := "\"\\/bfnrt".indexOf(escaped.toString()) ?! self.failure("JSON", "'\\" ++ escaped.toString() ++ "' isn't an escape")
out.push(['"', '\\', '/', '\u{8}', '\u{c}', '\n', '\r', '\t'][index]!!)
}
}
} uses #comptime.typeName
/// value - as a document gives it - made into Target, the way Target's declarations say (see core.md's
/// Documents); `where` names the place for an error - "the document.items[2].price"
local function decoded for Json<Target>(value: Unknown, where: String): ParseError ! Target {
return decodedAs(value, #comptime.typeName(Target), where)!! as Target
} uses #runtime.construct
/// the same, Target written as its text - "Array<Order>", "String?", "U8 | String"
local function decodedAs for Json(value: Unknown, typeText: String, where: String): ParseError ! Unknown {
let type := Type.named(typeText)
if typeText.endsWith("?") then {
return value == undefined ? undefined : decodedAs(value, typeText.stripSuffix("?")!!, where)!!
}
let alternatives := type.variants()
if type.isUnion() then {
return alternatives.map(λ(alternative) decodedAs(value, alternative.name(), where)).find(λ(result) not (result is Error)) ?! ParseError.new(where ++ " isn't any of " ++ typeText)
}
let name := type.name().split("<")[0]!!
let args := type.generics().map(λ(arg) arg.name())
if name == "Unknown" then {
return value
}
if value == undefined then {
error ParseError.new(where ++ " is missing - a " ++ typeText ++ " was expected")
}
if name == "String" then {
return value.narrow<String>() !? (error ParseError.new(where ++ " should be text, not " ++ value.toString()))
}
if name == "Boolean" then {
return value is Boolean ? value : Boolean.parse(value.toString()) !? (error ParseError.new(where ++ " should be true or false, not " ++ value.toString()))
}
if name == "Array" | "Set" | "Stack" | "Queue" | "Deque" then {
let items := value.narrow<Array<Unknown>>() !? (error ParseError.new(where ++ " should be a list, not " ++ value.toString()))
let decodedItems := items.enumerate().map(λ((i, item)) decodedAs(item, args[0] ?? "Unknown", where ++ "[" ++ i.toString() ++ "]")!!).toArray()
return decodedItems.collectInto(Type.named(name))
}
if name == "Map" | "SortedMap" then {
let members := value.narrow<Map<String, Unknown>>() !? (error ParseError.new(where ++ " should be an object, not " ++ value.toString()))
let out := members.entries().map(λ((key, member)) (decodedAs(key, args[0] ?? "String", where ++ "'s key " ++ key)!!, decodedAs(member, args[1] ?? "Unknown", where ++ "." ++ key)!!))
return out.collectInto(Type.named(name))
}
if type.isAssignableFrom(value.typeOf()) then {
return value
}
if type.implements(FromString) then {
return value.toString().parse(type) !? (error ParseError.new(where ++ " is " ++ value.toString() ++ ", which isn't a " ++ name))
}
if type.isStruct() then {
let members := value.narrow<Map<String, Unknown>>() !? (error ParseError.new(where ++ " should be an object with " ++ name ++ "'s fields, not " ++ value.toString()))
let fields := type.fields().entries().map(λ((field, fieldType)) (field, decodedAs(members[field], fieldType.name(), where ++ "." ++ field)!!))
return #runtime.construct(type, Map.fromIterable(fields))!!
}
if type.isEnum() and value is String then {
let variant := type.variants().find(λ(v) v.name() == name ++ "." ++ value) ?! ParseError.new(where ++ " is " ++ value ++ ", which isn't a " ++ name)
return #runtime.construct(variant, Map.of())!!
}
error ParseError.new(where ++ " is " ++ value.toString() ++ ", which can't be read as a " ++ typeText)
}/// the bytes a row of width pixels takes, at this many bits a pixel
local function rowBytesOf(bits: U64, width: U64): U64 {
return (bits * width + 7) ÷ 8
} uses #builtin.bytesUint
/// a number of width bytes at at - big-endian when big
local function uintAt(bytes: Bytes, at: U64, width: U64, big: Boolean := false): U64 {
return #builtin.bytesUint(bytes, at, width, big) as U64
}/// width bytes holding value - big-endian when big
local function uintBytes(value: U64, width: U64, big: Boolean := false): Bytes {
let out := Bytes.filled(width, 0)
#builtin.bytesPutUint(out, 0, width, value as F64, big)
return out
}/// every piece, one after another, copied once
local function joined(pieces: Array<Bytes>): Bytes {
let total: U64 := 0
for piece in pieces {
set total := total + piece.size()
}
let out := Bytes.filled(total, 0)
let at: U64 := 0
for piece in pieces {
out.copyFrom(piece, at)!!
set at := at + piece.size()
}
return out
}/// what can't be read, said as a ParseError about this format
local function unreadable(format: String, why: String): ParseError {
return ParseError.new(#format"not a #{ format } image this can read: #{ why }")
}/// a decoder's pixels, or why there are none
local function unpacked(pixels: Bytes?, format: String): ParseError ! Bytes {
if pixels == undefined then {
error unreadable(format, "its pixel data is cut short")
}
return pixels as Bytes
}/// the 8 bytes every PNG starts with
local function pngSignature(): Bytes {
return Bytes.fromArray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
}/// one PNG chunk: its data's length, its four-letter type, the data, and a CRC of the type and data
local function pngChunk(kind: String, data: Bytes): Bytes {
let typed := kind.encode() ++ data
return uintBytes(data.size(), 4, true) ++ typed ++ uintBytes((typed.crc32() as F64) as U64, 4, true)
} uses #builtin.pixelsUnpack, #builtin.bytesUint
/// IHDR, PLTE, tRNS and the IDATs joined - what APNG's frames are read with too
local function pngParts(bytes: Bytes, kinds: Array<String> := ["IDAT"]): ParseError ! PngParts {
let parts := PngParts{0, 0, 0, 0, false, Bytes.filled(0, 0), -1.0, Bytes.filled(0, 0)}
let data: Array<Bytes> := []
let at: U64 := 8
let size := bytes.size()
for _ in Iterator.forever() while at + 12 <= size {
let length := uintAt(bytes, at, 4, true)
if at + 12 + length > size then {
error unreadable("PNG", "a chunk runs past the end")
}
let tag := bytes.slice(at + 4..<at + 8)!!.decode()
let start := at + 8
if tag == "IHDR" then {
set parts.width := uintAt(bytes, start, 4, true)
set parts.height := uintAt(bytes, start + 4, 4, true)
set parts.depth := uintAt(bytes, start + 8, 1)
set parts.colorType := uintAt(bytes, start + 9, 1)
set parts.interlaced := uintAt(bytes, start + 12, 1) == 1
}
if tag == "PLTE" then {
set parts.palette := #builtin.pixelsUnpack(bytes, start, length ÷ 3, 1, LayoutRgb, length, false, Bytes.filled(0, 0), -1.0) ?? Bytes.filled(0, 0)
}
if tag == "tRNS" then {
if parts.colorType == 3 then {
for i in 0..<(length.min(parts.palette.size() ÷ 4)) {
set parts.palette[i * 4 + 3] := bytes[start + i] ?? 255
}
}
if parts.colorType == 0 and length >= 2 then {
set parts.key := #builtin.bytesUint(bytes, start, 2, true)
}
if parts.colorType == 2 and length >= 6 then {
let (r, g, b) := (#builtin.bytesUint(bytes, start, 2, true), #builtin.bytesUint(bytes, start + 2, 2, true), #builtin.bytesUint(bytes, start + 4, 2, true))
/// 8-bit samples' key is compared as 0xRRGGBB, 16-bit ones' as all 48 bits
set parts.key := if parts.depth == 16 then r * 4294967296.0 + g * 65536.0 + b else (r % 256.0) * 65536.0 + (g % 256.0) * 256.0 + (b % 256.0)
}
}
if kinds.contains(tag) then {
data.push(bytes.slice(start..<start + length)!!)
}
if tag == "IEND" then {
break _
}
set at := at + 12 + length
}
if parts.width == 0 or parts.height == 0 then {
error unreadable("PNG", "it has no header, or no pixels")
}
set parts.data := joined(data)
return parts
}/// the layout of a PNG color type's samples at this depth - undefined for one PNG doesn't have
local function pngLayoutOf(colorType: U64, depth: U64): U64? {
if colorType == 0 then {
return if depth == 1 then LayoutGray1 else if depth == 2 then LayoutGray2 else if depth == 4 then LayoutGray4 else if depth == 8 then LayoutGray else if depth == 16 then LayoutGray16 else undefined
}
if colorType == 3 then {
return if depth == 1 then LayoutIndex1 else if depth == 2 then LayoutIndex2 else if depth == 4 then LayoutIndex4 else if depth == 8 then LayoutIndex8 else undefined
}
if depth != 8 and depth != 16 then {
return undefined
}
let wide := depth == 16
return if colorType == 2 then (if wide then LayoutRgb16 else LayoutRgb) else if colorType == 4 then (if wide then LayoutGrayAlpha16 else LayoutGrayAlpha) else if colorType == 6 then (if wide then LayoutRgba16 else LayoutRgba) else undefined
}/// the layout PNG's samples are in, and their bits a pixel
local function pngLayout(colorType: U64, depth: U64): ParseError ! (U64, U64) {
let channels: U64 := if colorType == 0 then 1 else if colorType == 2 then 3 else if colorType == 3 then 1 else if colorType == 4 then 2 else if colorType == 6 then 4 else 0
let layout := pngLayoutOf(colorType, depth)
if layout == undefined then {
error unreadable("PNG", #format"color type #{ colorType } at #{ depth } bits a sample")
}
return (layout as U64, channels * depth)
} uses #builtin.zlibDecompress, #builtin.pngUnfilter, #builtin.pixelsUnpack, #builtin.adam7Place
/// a PNG's pixels, from its parts - each pass of an interlaced one in turn
local function pngPixels(parts: PngParts): ParseError ! Bytes {
let (layout, bits) := pngLayout(parts.colorType, parts.depth)!!
let raw := #builtin.zlibDecompress(parts.data)!!
/// how far back the filters look - a whole pixel, or a byte when pixels are smaller
let bpp := ((bits + 7) ÷ 8).max(1)
if not parts.interlaced then {
let rowBytes := rowBytesOf(bits, parts.width)
let samples := #builtin.pngUnfilter(raw, 0, rowBytes, parts.height, bpp)
if samples == undefined then {
error unreadable("PNG", "its rows are cut short, or one's filter isn't one")
}
return unpacked(#builtin.pixelsUnpack(samples as Bytes, 0, parts.width, parts.height, layout, rowBytes, false, parts.palette, parts.key), "PNG")!!
}
let rgba := Bytes.filled(parts.width * parts.height * 4, 0)
let offset: U64 := 0
/// Adam7: where each pass starts and steps, x then y
for pass in 0..<7 {
let x0: U64 := ([0, 4, 0, 2, 0, 1, 0])[pass]!!
let y0: U64 := ([0, 0, 4, 0, 2, 0, 1])[pass]!!
let dx: U64 := ([8, 8, 4, 4, 2, 2, 1])[pass]!!
let dy: U64 := ([8, 8, 8, 4, 4, 2, 2])[pass]!!
let w := if parts.width > x0 then (parts.width - x0 + dx - 1) ÷ dx else 0
let h := if parts.height > y0 then (parts.height - y0 + dy - 1) ÷ dy else 0
if w > 0 and h > 0 then {
let rowBytes := rowBytesOf(bits, w)
let samples := #builtin.pngUnfilter(raw, offset, rowBytes, h, bpp)
if samples == undefined then {
error unreadable("PNG", "an interlaced pass is cut short, or a row's filter isn't one")
}
let part := unpacked(#builtin.pixelsUnpack(samples as Bytes, 0, w, h, layout, rowBytes, false, parts.palette, parts.key), "PNG")!!
#builtin.adam7Place(rgba, parts.width, parts.height, part, pass)
set offset := offset + h * (rowBytes + 1)
}
}
return rgba
}/// every color type and bit depth, interlaced or not
local function decodePng(bytes: Bytes): ParseError ! Image {
let parts := pngParts(bytes)!!
return Image{parts.width, parts.height, pngPixels(parts)!!}
} uses #builtin.pixelsAnalyze, #builtin.pngFilter, #builtin.pixelsPack, #builtin.zlibCompress
/// the IDAT data of these pixels: gray, gray with alpha, RGB or RGBA - the fewest that hold them, or
/// RGBA always when rgba - each row with its best filter, deflated; and the color type that is
local function pngData(rgba: Bytes, width: U64, height: U64, alwaysRgba: Boolean := false): (Bytes, U64) {
let found := if alwaysRgba then 3 else #builtin.pixelsAnalyze(rgba) as U64
let alpha := found band 1 != 0
let color := found band 2 != 0
let (layout, colorType, bpp) := if color then (if alpha then (LayoutRgba, 6, 4) else (LayoutRgb, 2, 3)) else (if alpha then (LayoutGrayAlpha, 4, 2) else (LayoutGray, 0, 1))
let rowBytes := width * bpp
let filtered := #builtin.pngFilter(#builtin.pixelsPack(rgba, width, height, layout, rowBytes, false), rowBytes, height, bpp)
return (#builtin.zlibCompress(filtered), colorType)
}/// a palette BMP's colors, as RGBA - after the header, 4 bytes each (blue, green, red, unused)
local function bmpPalette(bytes: Bytes, infoSize: U64, offset: U64, bits: U64): Bytes {
let colors := if uintAt(bytes, 46, 4) > 0 then uintAt(bytes, 46, 4) else 1 shl bits
let room := if offset > 14 + infoSize then (offset - 14 - infoSize) ÷ 4 else 0
return #builtin.pixelsUnpack(bytes, 14 + infoSize, colors.min(room), 1, LayoutBgrx, colors * 4, false, Bytes.filled(0, 0), -1.0) ?? Bytes.filled(0, 0)
} uses #builtin.pixelsUnpack, #builtin.pixelsAnalyze
/// 1-, 4- and 8-bit with a palette, 16-bit (5-5-5 or 5-6-5), 24- and 32-bit, uncompressed - bottom row
/// first, or top row first when the height is negative
local function decodeBmp(bytes: Bytes): ParseError ! Image {
let size := bytes.size()
if size < 54 then {
error unreadable("BMP", "its headers are cut short")
}
let offset := uintAt(bytes, 10, 4)
let infoSize := uintAt(bytes, 14, 4)
let width := bytes.readInteger<S32>(18)!!
let height := bytes.readInteger<S32>(22)!!
let bits := uintAt(bytes, 28, 2)
let compression := uintAt(bytes, 30, 4)
if width < 0 then {
error unreadable("BMP", "a negative width")
}
let columns := width as U64
let rows := height.abs() as U64
let stride := (bits * columns + 31) ÷ 32 * 4
/// BI_BITFIELDS (3) and BI_ALPHABITFIELDS (6) give each channel's mask after the header
let masked := compression == 3 or compression == 6
if compression != 0 and not masked then {
error unreadable("BMP", #format"compression #{ compression }")
}
/// the red, green, blue and alpha masks follow a 40-byte header, or are in a longer one - at 54 either way
let mask := λ(k) if 58 + 4 * k <= size then uintAt(bytes, 54 + 4 * k, 4) else 0
let palette := if bits <= 8 then bmpPalette(bytes, infoSize, offset, bits) else Bytes.filled(0, 0)
let layout := if bits == 1 then LayoutIndex1 else if bits == 4 then LayoutIndex4 else if bits == 8 then LayoutIndex8 else if bits == 16 then (if masked and mask(0) == 0xF800 then LayoutRgb565 else LayoutRgb555) else if bits == 24 then LayoutBgr else if bits == 32 then (if masked and mask(3) == 0 then LayoutBgrx else LayoutBgra) else undefined
if layout == undefined then {
error unreadable("BMP", #format"#{ bits }-bit pixels")
}
let rgba := unpacked(#builtin.pixelsUnpack(bytes, offset, columns, rows, layout as U64, stride, height > 0, palette, -1.0), "BMP")!!
/// a 32-bit BMP whose alpha is 0 everywhere isn't using it - opaque, not invisible
if bits == 32 and (#builtin.pixelsAnalyze(rgba) as U64) band 8 == 0 then {
return Image{columns, rows, #builtin.pixelsUnpack(bytes, offset, columns, rows, LayoutBgrx, stride, height > 0, palette, -1.0) ?? rgba}
}
return Image{columns, rows, rgba}
} uses #builtin.qoiDecode
local function decodeQoi(bytes: Bytes): ParseError ! Image {
if bytes.size() < 22 then {
error unreadable("QOI", "its header is cut short")
}
let width := uintAt(bytes, 4, 4, true)
let height := uintAt(bytes, 8, 4, true)
return Image{width, height, unpacked(#builtin.qoiDecode(bytes, 14, width * height), "QOI")!!}
} uses #builtin.pixelsUnpack, #builtin.rleDecode
/// color-mapped, true-color and gray, plain or run-length encoded, 8 to 32 bits a pixel
local function decodeTga(bytes: Bytes): ParseError ! Image {
let size := bytes.size()
if size < 18 then {
error unreadable("TGA", "its header is cut short")
}
let idLength := uintAt(bytes, 0, 1)
let mapType := uintAt(bytes, 1, 1)
let kind := uintAt(bytes, 2, 1)
let mapFirst := uintAt(bytes, 3, 2)
let mapLength := uintAt(bytes, 5, 2)
let mapBits := uintAt(bytes, 7, 1)
let width := uintAt(bytes, 12, 2)
let height := uintAt(bytes, 14, 2)
let bits := uintAt(bytes, 16, 1)
let descriptor := uintAt(bytes, 17, 1)
let alphaBits := descriptor band 15
let mapStart := 18 + idLength
let mapBytes := if mapType == 1 then mapLength * ((mapBits + 7) ÷ 8) else 0
let at := mapStart + mapBytes
let bpp := (bits + 7) ÷ 8
/// a true-color layout for this many bits - 16 and 15 are 5-5-5, with an attribute bit as alpha when there's one
let trueColor := λ(b, alpha) if b == 32 then (if alpha then LayoutBgra else LayoutBgrx) else if b == 24 then LayoutBgr else if alpha then LayoutArgb1555 else LayoutRgb555
let base := kind band 7
/// the map starts at mapFirst - the indices below it have no color
let entries := #builtin.pixelsUnpack(bytes, mapStart, mapLength, 1, trueColor(mapBits, mapBits == 32 or mapBits == 16), mapBytes, false, Bytes.filled(0, 0), -1.0) ?? Bytes.filled(0, 0)
let palette := if mapType == 1 then Bytes.filled(mapFirst * 4, 0) ++ entries else Bytes.filled(0, 0)
let layout := if base == 1 then LayoutIndex8 else if base == 3 then (if bits == 16 then LayoutGrayAlpha else LayoutGray) else trueColor(bits, alphaBits > 0)
if (base == 1 and bits != 8) or (base == 2 and not [15, 16, 24, 32].contains(bits)) or (base == 3 and bits != 8 and bits != 16) or not [1, 2, 3].contains(base) then {
error unreadable("TGA", #format"image type #{ kind } at #{ bits } bits a pixel")
}
let raw := if kind >= 9 then unpacked(#builtin.rleDecode(bytes, at, width * height, bpp), "TGA")!! else bytes
let start := if kind >= 9 then 0 else at
/// the bottom row first unless the descriptor says the top
let image := Image{width, height, unpacked(#builtin.pixelsUnpack(raw, start, width, height, layout, width * bpp, descriptor band 0x20 == 0, palette, -1.0), "TGA")!!}
return if descriptor band 0x10 != 0 then image.flipHorizontal() else image
}local function isPnmSpace(byte: U64): Boolean {
return byte == 0x20 or byte == 0x09 or byte == 0x0A or byte == 0x0D
}/// the next whitespace-separated number from at - '#' starts a comment to the end of the line - and
/// where it ends
local function pnmNumber(bytes: Bytes, at: U64): ParseError ! (U64, U64) {
let i := at
let size := bytes.size()
for _ in Iterator.forever() while i < size {
let byte := (bytes[i] ?? 0) as U64
if byte == 0x23 then {
for _ in Iterator.forever() while i < size and bytes[i] != 0x0A {
set i := i + 1
}
} else if isPnmSpace(byte) then {
set i := i + 1
} else {
break _
}
}
let value: U64 := 0
let start := i
for _ in Iterator.forever() while i < size {
let byte := (bytes[i] ?? 0) as U64
if byte < 0x30 or byte > 0x39 then {
break _
}
set value := value * 10 + (byte - 0x30)
set i := i + 1
}
if i == start then {
error unreadable("PNM", "a number is missing from its header")
}
return (value, i)
}/// samples whose most is maxval, as 0 to 255
local function scaledSamples(samples: Array<U64>, maxval: U64): Bytes {
return Bytes.fromArray(samples.map(λ(v) (v.min(maxval) * 255 + maxval ÷ 2) ÷ maxval.max(1)))
}/// P1 to P6, and P7 (PAM) with 1 to 4 channels - plain (ASCII) or raw, any maxval
local function decodePnm(bytes: Bytes): ParseError ! Image {
let kind := ((bytes[1] ?? 0) as U64) - 0x30
let width: U64 := 0
let height: U64 := 0
let maxval: U64 := 1
let channels: U64 := if kind == 3 or kind == 6 then 3 else 1
let at: U64 := 2
if kind == 7 then {
/// PAM: a line each - WIDTH, HEIGHT, DEPTH, MAXVAL, TUPLTYPE - then ENDHDR
let headerEnd := bytes.indexOf("ENDHDR".encode()) ?? 0
if headerEnd == 0 then {
error unreadable("PAM", "its header has no ENDHDR")
}
for line in bytes.slice(3..<headerEnd)!!.decode().split("\n") {
let words := line.trim().split(" ").filter(λ(w) w != "")
let value := U64.parse(words[1] ?? "0") !? 0
match words[0] ?? "" {
case "WIDTH" then {
set width := value
}
case "HEIGHT" then {
set height := value
}
case "DEPTH" then {
set channels := value
}
case "MAXVAL" then {
set maxval := value
}
case _ then {
}
}
}
set at := (bytes.indexOf("\n".encode(), headerEnd) ?? headerEnd) + 1
} else {
let (w, afterWidth) := pnmNumber(bytes, at)!!
let (h, afterHeight) := pnmNumber(bytes, afterWidth)!!
set width := w
set height := h
set at := afterHeight
if kind != 1 and kind != 4 then {
let (m, afterMax) := pnmNumber(bytes, at)!!
set maxval := m
set at := afterMax
}
/// a single whitespace byte before raw data
set at := at + 1
}
if width == 0 or height == 0 or channels < 1 or channels > 4 then {
error unreadable("PNM", "its size or depth is missing")
}
let layouts := [LayoutGray, LayoutGrayAlpha, LayoutRgb, LayoutRgba]
let wide := maxval > 255
let layout := if wide then ([LayoutGray16, LayoutGrayAlpha16, LayoutRgb16, LayoutRgba16])[channels - 1]!! else layouts[channels - 1]!!
/// bitmaps: 1 is black
let blackWhite := Bytes.fromArray([255, 255, 255, 255, 0, 0, 0, 255])
if kind == 4 then {
let rgba := #builtin.pixelsUnpack(bytes, at, width, height, LayoutIndex1, (width + 7) ÷ 8, false, blackWhite, -1.0)
return Image{width, height, unpacked(rgba, "PBM")!!}
}
if kind <= 3 then {
/// plain: every sample a number
let samples: Array<U64> := []
let count := width * height * channels
let i := at - 1
if kind == 1 then {
/// a bitmap's digits needn't be apart
for _ in Iterator.forever() while samples.size() < count and i < bytes.size() {
let byte := (bytes[i] ?? 0) as U64
if byte == 0x30 or byte == 0x31 then {
samples.push(if byte == 0x31 then 0 else 255)
}
set i := i + 1
}
return Image{width, height, unpacked(#builtin.pixelsUnpack(Bytes.fromArray(samples), 0, width, height, LayoutGray, width, false, Bytes.filled(0, 0), -1.0), "PBM")!!}
}
for _ in 0..<count {
let (value, next) := pnmNumber(bytes, i)!!
samples.push(value)
set i := next
}
return Image{width, height, unpacked(#builtin.pixelsUnpack(scaledSamples(samples, maxval), 0, width, height, layouts[channels - 1]!!, width * channels, false, Bytes.filled(0, 0), -1.0), "PNM")!!}
}
let sampleBytes: U64 := if wide then 2 else 1
let rowBytes := width * channels * sampleBytes
let rgba := unpacked(#builtin.pixelsUnpack(bytes, at, width, height, layout, rowBytes, false, Bytes.filled(0, 0), -1.0), "PNM")!!
if maxval == 255 or maxval == 65535 then {
return Image{width, height, rgba}
}
/// another maxval: each sample scaled to 0 to 255, one at a time
let samples: Array<U64> := []
for k in 0..<(width * height * channels) {
samples.push(uintAt(bytes, at + k * sampleBytes, sampleBytes, true))
}
return Image{width, height, unpacked(#builtin.pixelsUnpack(scaledSamples(samples, maxval), 0, width, height, layouts[channels - 1]!!, width * channels, false, Bytes.filled(0, 0), -1.0), "PNM")!!}
}/// a palette of colors RGB triplets at at, as RGBA quads
local function gifPalette(bytes: Bytes, at: U64, colors: U64): Bytes {
return #builtin.pixelsUnpack(bytes, at, colors, 1, LayoutRgb, colors * 3, false, Bytes.filled(0, 0), -1.0) ?? Bytes.filled(colors * 4, 0)
}/// past the sub-blocks starting at at - the index after their empty one
local function pastSubBlocks(bytes: Bytes, at: U64): U64 {
let i := at
let size := bytes.size()
for _ in Iterator.forever() while i < size and bytes[i] != 0 {
set i := i + ((bytes[i] ?? 0) as U64) + 1
}
return i + 1
}/// an interlaced frame's rows put in order: GIF stores every 8th from 0, every 8th from 4, every 4th from
/// 2, then every 2nd from 1
local function deinterlaced(rgba: Bytes, width: U64, height: U64): Bytes {
let out := Bytes.filled(rgba.size(), 0)
let row: U64 := 0
for pass in 0..<4 {
let step: U64 := ([8, 8, 4, 2])[pass]!!
let y: U64 := ([0, 4, 2, 1])[pass]!!
for _ in Iterator.forever() while y < height {
out.copyFrom(rgba.slice(row * width * 4..<(row + 1) * width * 4)!!, y * width * 4)!!
set row := row + 1
set y := y + step
}
}
return out
} uses #builtin.lzwDecode, #builtin.pixelsUnpack
/// every frame of a GIF, as it's stored - each over the frames before is the reader's to compose
local function gifParts(bytes: Bytes): ParseError ! GifParts {
let size := bytes.size()
if size < 13 then {
error unreadable("GIF", "its header is cut short")
}
let parts := GifParts{uintAt(bytes, 6, 2), uintAt(bytes, 8, 2), undefined, []}
let flags := uintAt(bytes, 10, 1)
let at: U64 := 13
let global := if flags band 0x80 != 0 then gifPalette(bytes, 13, 1 shl ((flags band 7) + 1)) else Bytes.filled(0, 0)
if flags band 0x80 != 0 then {
set at := at + 3 * (1 shl ((flags band 7) + 1))
}
let delay: U64 := 0
let disposal: U64 := 0
let transparent: U64? := undefined
for _ in Iterator.forever() while at < size {
let kind := uintAt(bytes, at, 1)
if kind == 0x3B then {
break _
}
if kind == 0x21 then {
let label := uintAt(bytes, at + 1, 1)
if label == 0xF9 and at + 7 < size then {
let packed := uintAt(bytes, at + 3, 1)
set disposal := (packed shr 2) band 7
set delay := uintAt(bytes, at + 4, 2)
set transparent := if packed band 1 != 0 then uintAt(bytes, at + 6, 1) else undefined
}
/// NETSCAPE2.0's loop count
if label == 0xFF and at + 16 < size and bytes.slice(at + 3..<at + 14)!!.decode() == "NETSCAPE2.0" then {
set parts.loops := uintAt(bytes, at + 16, 2)
}
set at := pastSubBlocks(bytes, at + 2)
} else if kind == 0x2C then {
if at + 10 > size then {
error unreadable("GIF", "an image descriptor is cut short")
}
let left := uintAt(bytes, at + 1, 2)
let top := uintAt(bytes, at + 3, 2)
let width := uintAt(bytes, at + 5, 2)
let height := uintAt(bytes, at + 7, 2)
let packed := uintAt(bytes, at + 9, 1)
set at := at + 10
let palette := if packed band 0x80 != 0 then gifPalette(bytes, at, 1 shl ((packed band 7) + 1)) else global.slice(0..<global.size())!!
if packed band 0x80 != 0 then {
set at := at + 3 * (1 shl ((packed band 7) + 1))
}
if transparent != undefined and (transparent as U64) * 4 < palette.size() then {
set palette[(transparent as U64) * 4 + 3] := 0
}
let minCodeSize := uintAt(bytes, at, 1)
let indices := #builtin.lzwDecode(bytes, at + 1, minCodeSize, width * height)
if indices == undefined or minCodeSize > 11 then {
error unreadable("GIF", "a frame's pixels are cut short")
}
let rgba := unpacked(#builtin.pixelsUnpack(indices as Bytes, 0, width, height, LayoutIndex8, width, false, palette, -1.0), "GIF")!!
parts.frames.push(GifFrame{left, top, width, height, delay, disposal, if packed band 0x40 != 0 then deinterlaced(rgba, width, height) else rgba})
set at := pastSubBlocks(bytes, at + 1)
set delay := 0
set disposal := 0
set transparent := undefined
} else {
error unreadable("GIF", #format"a block of kind #{ kind }")
}
}
if parts.frames.isEmpty() then {
error unreadable("GIF", "it has no frames")
}
return parts
} uses #builtin.pixelsDraw
/// a GIF's first frame, on its screen - transparent where the frame doesn't cover it
local function decodeGifImage(bytes: Bytes): ParseError ! Image {
let parts := gifParts(bytes)!!
let canvas := Image.new(parts.width, parts.height)
let frame := parts.frames[0]!!
#builtin.pixelsDraw(canvas.rgba, canvas.columns, canvas.rows, frame.rgba, frame.width, frame.height, frame.left as S64, frame.top as S64)
return canvas
} uses #builtin.quantize, #builtin.pixelsPack, #builtin.lzwEncode
/// a GIF's image data for these pixels: at most 256 colors - dithered when there are more - an index
/// for transparency when a pixel's alpha is under 128. Its local color table's bits, the table, and
/// the transparent index (or undefined)
local function gifImageData(rgba: Bytes, width: U64, height: U64): (U64, Bytes, U64?, Bytes) {
let quantized := #builtin.quantize(rgba, width * height, width, 256, true)
let palette := quantized[0]!! as Bytes
let indices := quantized[1]!! as Bytes
let transparent := quantized[2]!! as F64
let colors := (palette.size() ÷ 4).max(2)
/// a table of 2^bits colors, bits at least 1
let bits: U64 := 1
for _ in Iterator.forever() while (1 shl bits) < colors {
set bits := bits + 1
}
let table := #builtin.pixelsPack(palette, palette.size() ÷ 4, 1, LayoutRgb, palette.size() ÷ 4 * 3, false) ++ Bytes.filled(((1 shl bits) - palette.size() ÷ 4) * 3, 0)
let minCodeSize := bits.max(2)
let data := Bytes.fromArray([minCodeSize as U8]) ++ #builtin.lzwEncode(indices, minCodeSize)
return (bits, table, if transparent < 0.0 then undefined else transparent as U64, data)
}/// one GIF frame: its graphic control extension (when it has a delay, a disposal or transparency), its
/// image descriptor with a local color table, and its image data
local function gifFrameBytes(rgba: Bytes, width: U64, height: U64, left: U64, top: U64, delay: U64, disposal: U64): Bytes {
let (bits, table, transparent, data) := gifImageData(rgba, width, height)
let control := Bytes.filled(0, 0)
if transparent != undefined or delay > 0 or disposal > 0 then {
let packed := (disposal shl 2) bor (if transparent != undefined then 1 else 0)
set control := Bytes.fromArray([0x21, 0xF9, 4, packed as U8]) ++ uintBytes(delay, 2) ++ Bytes.fromArray([(transparent ?? 0) as U8, 0])
}
let descriptor := Bytes.fromArray([0x2C]) ++ uintBytes(left, 2) ++ uintBytes(top, 2) ++ uintBytes(width, 2) ++ uintBytes(height, 2) ++ Bytes.fromArray([(0x80 bor (bits - 1)) as U8])
return control ++ descriptor ++ table ++ data
}/// GIF89a's header and logical screen, with no global color table - each frame has its own
local function gifHeader(width: U64, height: U64): Bytes {
return "GIF89a".encode() ++ uintBytes(width, 2) ++ uintBytes(height, 2) ++ Bytes.fromArray([0, 0, 0])
}/// a marker's segment: FF, the marker, its length (2 more than its data) and its data
local function jpegSegment(marker: U8, data: Bytes): Bytes {
return Bytes.fromArray([0xFF, marker]) ++ uintBytes(data.size() + 2, 2, true) ++ data
}/// Annex K's table scaled for quality as the IJG's encoder does: 50 is the table itself, 100 every
/// value 1, below 50 coarser and coarser
local function jpegQuantTable(base: Array<U64>, quality: U64): Array<U64> {
let scale := if quality < 50 then 5000 ÷ quality else 200 - 2 * quality
return base.map(λ(v) ((v * scale + 50) ÷ 100).max(1).min(255))
}/// 4-byte little-endian numbers, as the decoding kernels read them
local function int32s(values: Array<U64>): Bytes {
let out := Bytes.filled(values.size() * 4, 0)
for value, i in values {
#builtin.bytesPutUint(out, i * 4, 4, value as F64, false)
}
return out
}/// The frame as jpegDecodeScan and jpegFinish read it: the component count, the MCUs across and down,
/// the largest sampling across and down, whether it's progressive - then for each component its
/// sampling, its quantization table, its blocks across and down (whole MCUs' worth), where its
/// coefficients start, and its blocks across and down the image itself covers. And how many
/// coefficients there are in all.
local function jpegFrame(width: U64, height: U64, components: Array<JpegComponent>, progressive: Boolean): (Bytes, U64) {
let maxH := components.map(λ(c) c.h).max() !? 1
let maxV := components.map(λ(c) c.v).max() !? 1
let mcusX := (width + 8 * maxH - 1) ÷ (8 * maxH)
let mcusY := (height + 8 * maxV - 1) ÷ (8 * maxV)
let values: Array<U64> := [components.size(), mcusX, mcusY, maxH, maxV, if progressive then 1 else 0]
let offset: U64 := 0
for c in components {
let perLine := mcusX * c.h
let perColumn := mcusY * c.v
let across := ((width * c.h + maxH - 1) ÷ maxH + 7) ÷ 8
let down := ((height * c.v + maxV - 1) ÷ maxV + 7) ÷ 8
values.pushAll([c.h, c.v, c.table, perLine, perColumn, offset, across, down])
set offset := offset + perLine * perColumn * 64
}
return (int32s(values), offset)
} uses #builtin.bytesPutUint, #builtin.bytesUint, #builtin.jpegDecodeScan, #builtin.jpegFinish
/// what the bytes hold, read marker by marker - each scan's coefficients added to the last's
local function decodeJpeg(bytes: Bytes): ParseError ! Image {
let size := bytes.size()
let byte := λ(at) (bytes[at] ?? 0) as U64
let quant := Bytes.filled(512, 0)
let tables := Bytes.filled(8 * 272, 0)
/// Annex K's tables to begin with - Motion JPEG's frames often leave them out and count on them
for slot, i in [0, 4, 1, 5] {
tables.copyFrom(Bytes.fromArray(jpegHuffman.slice(jpegHuffmanStarts[i]!!..<jpegHuffmanStarts[i + 1]!!) !? []), slot * 272)!!
}
let width: U64 := 0
let height: U64 := 0
let components: Array<JpegComponent> := []
let frame := Bytes.filled(0, 0)
let coeffs := Bytes.filled(0, 0)
let restart: U64 := 0
/// Adobe's APP14 says how 3 or 4 components are coded - undefined when there's none
let transform: U64? := undefined
let at: U64 := 2
for _ in Iterator.forever() while at + 4 <= size {
if byte(at) != 0xFF then {
error unreadable("JPEG", #format"there's no marker at #{ at }")
}
let marker := byte(at + 1)
/// fill bytes, and markers with no segment
if marker == 0xFF then {
set at := at + 1
continue _
}
if marker == 0xD8 or marker == 0x01 or (marker >= 0xD0 and marker <= 0xD7) then {
set at := at + 2
continue _
}
if marker == 0xD9 then {
break _
}
let start := at + 4
let end := at + 2 + uintAt(bytes, at + 2, 2, true)
if end > size then {
error unreadable("JPEG", "a segment runs past the end")
}
if marker == 0xC0 or marker == 0xC1 or marker == 0xC2 then {
if byte(start) != 8 then {
error unreadable("JPEG", #format"#{ byte(start) }-bit samples")
}
set height := uintAt(bytes, start + 1, 2, true)
set width := uintAt(bytes, start + 3, 2, true)
for i in 0..<byte(start + 5) {
let c := start + 6 + i * 3
components.push(JpegComponent{byte(c), byte(c + 1) shr 4, byte(c + 1) band 15, byte(c + 2) band 3})
}
if width == 0 or height == 0 or components.isEmpty() or components.any(λ(c) c.h < 1 or c.h > 4 or c.v < 1 or c.v > 4) then {
error unreadable("JPEG", "its frame header doesn't make sense")
}
let (made, count) := jpegFrame(width, height, components, marker == 0xC2)
set frame := made
set coeffs := Bytes.filled(count * 2, 0)
} else if marker >= 0xC3 and marker <= 0xCF and marker != 0xC4 and marker != 0xC8 and marker != 0xCC then {
error unreadable("JPEG", "it's lossless or arithmetic-coded")
} else if marker == 0xC4 then {
/// each table: its class and number, 16 counts, its values - into its slot, DC 0 to 3 then AC 4 to 7
let i := start
for _ in Iterator.forever() while i + 17 <= end {
let slot := (if byte(i) shr 4 == 0 then 0 else 4) + (byte(i) band 3)
let count: U64 := 0
for k in 0..<16 {
set count := count + byte(i + 1 + k)
}
tables.copyFrom(Bytes.filled(272, 0), slot * 272)!!
tables.copyFrom(bytes.slice(i + 1..<(i + 17 + count).min(end))!!, slot * 272)!!
set i := i + 17 + count
}
} else if marker == 0xDB then {
/// each table: its precision (8 or 16 bits) and number, then 64 values in zigzag order
let i := start
for _ in Iterator.forever() while i < end {
let wide := byte(i) shr 4 != 0
let table := byte(i) band 3
let width := if wide then 2 else 1
for k in 0..<64 {
#builtin.bytesPutUint(quant, (table * 64 + jpegZigzag[k]!!) * 2, 2, #builtin.bytesUint(bytes, i + 1 + k * width, width, true), false)
}
set i := i + 1 + 64 * width
}
} else if marker == 0xDD then {
set restart := uintAt(bytes, start, 2, true)
} else if marker == 0xEE and end - start >= 12 and bytes.slice(start..<start + 5)!!.decode() == "Adobe" then {
set transform := byte(start + 11)
} else if marker == 0xDA then {
if components.isEmpty() then {
error unreadable("JPEG", "a scan comes before its frame")
}
let count := byte(start)
let values: Array<U64> := [count]
for i in 0..<count {
let id := byte(start + 1 + i * 2)
let index := components.findIndex(λ(c) c.id == id)
if index == undefined then {
error unreadable("JPEG", #format"a scan names component #{ id }, which the frame doesn't have")
}
let selectors := byte(start + 2 + i * 2)
values.pushAll([index as U64, (selectors shr 4) band 3, selectors band 3])
}
let spectrum := start + 1 + count * 2
values.pushAll([byte(spectrum), byte(spectrum + 1), byte(spectrum + 2) shr 4, byte(spectrum + 2) band 15, restart])
let next := #builtin.jpegDecodeScan(bytes, end, frame, int32s(values), tables, coeffs)
if next < 0.0 then {
error unreadable("JPEG", "a scan uses a Huffman table it doesn't have")
}
set at := next as U64
continue _
}
set at := end
}
if components.isEmpty() then {
error unreadable("JPEG", "it has no frame")
}
/// three components are YCbCr unless Adobe says RGB or their ids spell R, G, B; four are CMYK unless Adobe says YCCK
let ids := components.map(λ(c) c.id)
let colors := transform ?? (if components.size() == 3 then (if ids == [82, 71, 66] then 0 else 1) else 0)
return Image{width, height, #builtin.jpegFinish(coeffs, frame, quant, width, height, colors)}
}/// a region of an image (RGBA, width wide) set to pixels (w x h at x, y), row by row - or cleared, when
/// pixels is undefined
local function putRegion(rgba: Bytes, width: U64, height: U64, pixels: Bytes?, x: U64, y: U64, w: U64, h: U64): Undefined {
let rows := h.min(if y < height then height - y else 0)
let across := w.min(if x < width then width - x else 0)
for row in 0..<rows {
let line := if pixels == undefined then Bytes.filled(across * 4, 0) else (pixels as Bytes).slice(row * w * 4..<(row * w + across) * 4)!!
rgba.copyFrom(line, ((y + row) * width + x) * 4)!!
}
} uses #builtin.pixelsDraw
/// every frame, composed: each drawn over what the last left, then disposed of as it says - 2 cleared
/// to transparent, 3 back to what was there before it
local function decodeGifVideo(bytes: Bytes): ParseError ! Video {
let parts := gifParts(bytes)!!
let video := Video.new(parts.width, parts.height, if parts.loops == undefined then 1 else (if parts.loops == 0 then 0 else (parts.loops as U64) + 1))
let canvas := Bytes.filled(parts.width * parts.height * 4, 0)
for frame in parts.frames {
let before := if frame.disposal == 3 then canvas.slice(0..<canvas.size())!! else canvas
#builtin.pixelsDraw(canvas, parts.width, parts.height, frame.rgba, frame.width, frame.height, frame.left as S64, frame.top as S64)
/// what browsers do: a delay under 2 hundredths shows for a tenth of a second
let seconds := if frame.delay < 2 then 0.1 else (frame.delay as F64) / 100.0
video.add(Image{parts.width, parts.height, canvas.slice(0..<canvas.size())!!}, seconds) !? undefined
if frame.disposal == 2 then {
putRegion(canvas, parts.width, parts.height, undefined, frame.left, frame.top, frame.width, frame.height)
}
if frame.disposal == 3 then {
set canvas := before
}
}
return video
} uses #builtin.pixelsDraw
/// every frame, composed as APNG says: drawn over the last (blend 1) or in its place (0), then left
/// (dispose 0), cleared (1) or put back to what was there before (2). A PNG with no animation is one frame.
local function decodeApng(bytes: Bytes): ParseError ! Video {
let parts := pngParts(bytes, [])!!
let frames: Array<ApngFrame> := []
let plays: U64 := 1
let animated := false
let at: U64 := 8
let size := bytes.size()
for _ in Iterator.forever() while at + 12 <= size {
let length := uintAt(bytes, at, 4, true)
let tag := bytes.slice(at + 4..<at + 8)!!.decode()
let start := at + 8
if at + 12 + length > size or tag == "IEND" then {
break _
}
if tag == "acTL" then {
set animated := true
set plays := uintAt(bytes, start + 4, 4, true)
}
if tag == "fcTL" and length >= 26 then {
let den := uintAt(bytes, start + 22, 2, true)
let seconds := (uintAt(bytes, start + 20, 2, true) as F64) / (if den == 0 then 100.0 else den as F64)
frames.push(ApngFrame{uintAt(bytes, start + 4, 4, true), uintAt(bytes, start + 8, 4, true), uintAt(bytes, start + 12, 4, true), uintAt(bytes, start + 16, 4, true), seconds, uintAt(bytes, start + 24, 1), uintAt(bytes, start + 25, 1), []})
}
/// IDAT is the first frame's only when an fcTL came before it
if tag == "IDAT" and not frames.isEmpty() then {
frames[frames.size() - 1]!!.data.push(bytes.slice(start..<start + length)!!)
}
if tag == "fdAT" and not frames.isEmpty() and length >= 4 then {
frames[frames.size() - 1]!!.data.push(bytes.slice(start + 4..<start + length)!!)
}
set at := at + 12 + length
}
let video := Video.new(parts.width, parts.height, plays)
if not animated or frames.isEmpty() then {
video.add(Image{parts.width, parts.height, pngPixels(parts)!!}, 0.0) !? undefined
return video
}
let canvas := Bytes.filled(parts.width * parts.height * 4, 0)
for frame in frames {
let before := if frame.dispose == 2 then canvas.slice(0..<canvas.size())!! else canvas
let framed := PngParts{frame.width, frame.height, parts.depth, parts.colorType, parts.interlaced, parts.palette, parts.key, joined(frame.data)}
let pixels := pngPixels(framed)!!
if frame.blend == 1 then {
#builtin.pixelsDraw(canvas, parts.width, parts.height, pixels, frame.width, frame.height, frame.left as S64, frame.top as S64)
} else {
putRegion(canvas, parts.width, parts.height, pixels, frame.left, frame.top, frame.width, frame.height)
}
video.add(Image{parts.width, parts.height, canvas.slice(0..<canvas.size())!!}, frame.seconds) !? undefined
if frame.dispose == 1 then {
putRegion(canvas, parts.width, parts.height, undefined, frame.left, frame.top, frame.width, frame.height)
}
if frame.dispose == 2 then {
set canvas := before
}
}
return video
}/// the line from at, and the index after its newline
local function lineAt(bytes: Bytes, at: U64): (String, U64) {
let end := bytes.indexOf("\n".encode(), at) ?? bytes.size()
return (bytes.slice(at..<end)!!.decode(), end + 1)
} uses #builtin.yuvDecode
/// 4:2:0 (any siting), 4:2:2, 4:4:4 and gray, 8 bits a sample
local function decodeY4m(bytes: Bytes): ParseError ! Video {
let (header, start) := lineAt(bytes, 0)
let width: U64 := 0
let height: U64 := 0
let num: U64 := 25
let den: U64 := 1
let mode: U64 := 0
for part in header.split(" ").drop(1) {
let kind := part.charAt(0) ?? ""
let rest := part.slice(1..<part.size())!!
if kind == "W" then {
set width := U64.parse(rest) !? 0
}
if kind == "H" then {
set height := U64.parse(rest) !? 0
}
if kind == "F" then {
let fraction := rest.split(":")
set num := U64.parse(fraction[0] ?? "25") !? 25
set den := U64.parse(fraction[1] ?? "1") !? 1
}
if kind == "C" then {
if rest.startsWith("420") then {
set mode := 0
} else if rest == "422" then {
set mode := 1
} else if rest == "444" then {
set mode := 2
} else if rest == "mono" then {
set mode := 3
} else {
error unreadable("Y4M", #format"its colorspace C#{ rest }")
}
}
}
if width == 0 or height == 0 then {
error unreadable("Y4M", "its header has no size")
}
let video := Video.new(width, height)
let seconds := (den as F64) / (num.max(1) as F64)
let at := start
for _ in Iterator.forever() while at < bytes.size() {
let (line, data) := lineAt(bytes, at)
if not line.startsWith("FRAME") then {
error unreadable("Y4M", "a frame doesn't start with FRAME")
}
let rgba := #builtin.yuvDecode(bytes, data, width, height, mode)
if rgba == undefined then {
break _
}
video.add(Image{width, height, rgba as Bytes}, seconds) !? undefined
let chroma := if mode == 3 then 0 else (if mode == 2 then width else (width + 1) ÷ 2) * (if mode == 0 then (height + 1) ÷ 2 else height)
set at := data + width * height + 2 * chroma
}
return video
}/// Motion JPEG ("MJPG") or uncompressed (DIB) frames of the first video stream, at its frame rate
local function decodeAvi(bytes: Bytes): ParseError ! Video {
let size := bytes.size()
let width: U64 := 0
let height: U64 := 0
let bits: U64 := 24
let compression := ""
let seconds := 1.0 / 25.0
let video: Video? := undefined
let found := false
let at: U64 := 12
/// chunks and lists, depth first - a list's own chunks right after its kind
for _ in Iterator.forever() while at + 8 <= size {
let kind := bytes.slice(at..<at + 4)!!.decode()
let length := uintAt(bytes, at + 4, 4)
let start := at + 8
if kind == "LIST" or kind == "RIFF" then {
set at := start + 4
continue _
}
if kind == "strh" and not found and bytes.slice(start..<start + 4)!!.decode() == "vids" then {
let scale := uintAt(bytes, start + 20, 4)
let rate := uintAt(bytes, start + 24, 4)
if rate > 0 then {
set seconds := (scale as F64) / (rate as F64)
}
}
if kind == "strf" and not found then {
set found := true
set width := uintAt(bytes, start + 4, 4)
set height := uintAt(bytes, start + 8, 4)
set bits := uintAt(bytes, start + 14, 2)
set compression := bytes.slice(start + 16..<start + 20)!!.decode()
set video := Video.new(width, height, 1)
}
if (kind == "00dc" or kind == "00db") and video != undefined and length > 0 then {
let data := bytes.slice(start..<(start + length).min(size))!!
let frame := if compression == "MJPG" or compression == "mjpg" or compression == "JPEG" then decodeJpeg(data)!! else Image{width, height, unpacked(#builtin.pixelsUnpack(data, 0, width, height, if bits == 32 then LayoutBgrx else LayoutBgr, (bits * width + 31) ÷ 32 * 4, true, Bytes.filled(0, 0), -1.0), "AVI")!!}
(video as Video).add(frame, seconds) !? undefined
}
set at := start + length + length % 2
}
if video == undefined then {
error unreadable("AVI", "it has no video stream")
}
return video as Video
}/// a duration in seconds as APNG's 16-bit numerator and denominator - 1 / n when that's what it is (1 / 30
/// for 30 fps), thousandths otherwise
local function delayFraction(seconds: F64): (U64, U64) {
if seconds > 0.0 then {
let inverse := 1.0 / seconds
if (inverse - inverse.round()).abs() < 0.000001 and inverse.round() <= 65535.0 then {
return (1, inverse.round() as U64)
}
}
return ((seconds * 1000.0).round().max(0.0).min(65535.0) as U64, 1000)
}/// a frame rate as a whole numerator and denominator - fps itself over 1 when it's whole, thousandths otherwise
local function rateFraction(fps: F64): (U64, U64) {
if fps == fps.round() then {
return (fps as U64, 1)
}
return ((fps * 1000.0).round() as U64, 1000)
}/// NETSCAPE2.0's loop count: how many times it repeats after the first, 0 forever - nothing when it plays once
local function gifLoops(plays: U64): Bytes {
if plays == 1 then {
return Bytes.filled(0, 0)
}
let repeats := if plays == 0 then 0 else plays - 1
return Bytes.fromArray([0x21, 0xFF, 11]) ++ "NETSCAPE2.0".encode() ++ Bytes.fromArray([3, 1]) ++ uintBytes(repeats, 2) ++ Bytes.fromArray([0])
}/// a 4-byte big-endian number
local function word(value: U64): Bytes {
return uintBytes(value, 4, true)
}local function apngBytes(self: VideoEncoder): Bytes {
let pieces: Array<Bytes> := [Bytes.fromArray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])]
pieces.push(pngChunk("IHDR", word(self.columns) ++ word(self.rows) ++ Bytes.fromArray([8, 6, 0, 0, 0])))
pieces.push(pngChunk("acTL", word(self.chunks.size()) ++ word(self.plays)))
let sequence: U64 := 0
for data, i in self.chunks {
/// the frame's sequence number, size and place, its delay as a fraction of a second, no disposal, drawn
/// over nothing (source)
let (num, den) := self.delays[i]!!
let control := word(sequence) ++ word(self.columns) ++ word(self.rows) ++ word(0) ++ word(0) ++ uintBytes(num, 2, true) ++ uintBytes(den, 2, true) ++ Bytes.fromArray([0, 0])
pieces.push(pngChunk("fcTL", control))
set sequence := sequence + 1
if i == 0 then {
pieces.push(pngChunk("IDAT", data))
} else {
pieces.push(pngChunk("fdAT", word(sequence) ++ data))
set sequence := sequence + 1
}
}
pieces.push(pngChunk("IEND", Bytes.filled(0, 0)))
return joined(pieces)
}/// a RIFF chunk: its four letters, its size, its data - padded to an even length
local function riffChunk(kind: String, data: Bytes): Bytes {
let padding := if data.size() % 2 == 1 then Bytes.filled(1, 0) else Bytes.filled(0, 0)
return kind.encode() ++ uintBytes(data.size(), 4) ++ data ++ padding
}/// a RIFF list: LIST, its size, its kind, its chunks
local function riffList(kind: String, chunks: Bytes): Bytes {
return "LIST".encode() ++ uintBytes(chunks.size() + 4, 4) ++ kind.encode() ++ chunks
}local function aviBytes(self: VideoEncoder): Bytes {
let count := self.chunks.size()
let (rate, scale) := rateFraction(self.rate)
let largest := self.chunks.map(λ(c) c.size()).max() !? 0
let le := λ(value, width) uintBytes(value, width)
/// the main header: microseconds a frame, the most bytes a second, padding, AVIF_HASINDEX, the frames,
/// none before the first, one stream, the biggest frame, the size, 4 reserved
let mainHeader := le(((1000000.0 / self.rate).round()) as U64, 4) ++ le(largest * (self.rate.ceil() as U64), 4) ++ le(0, 4) ++ le(0x10, 4) ++ le(count, 4) ++ le(0, 4) ++ le(1, 4) ++ le(largest, 4) ++ le(self.columns, 4) ++ le(self.rows, 4) ++ Bytes.filled(16, 0)
/// the stream's header: video, MJPG, no flags, priority and language, frames before, scale and rate (fps =
/// rate / scale), start, length, the biggest frame, quality -1 (the default), any sample size, its rectangle
let streamHeader := "vids".encode() ++ "MJPG".encode() ++ le(0, 4) ++ le(0, 4) ++ le(0, 4) ++ le(scale, 4) ++ le(rate, 4) ++ le(0, 4) ++ le(count, 4) ++ le(largest, 4) ++ le(0xFFFFFFFF, 4) ++ le(0, 4) ++ le(0, 2) ++ le(0, 2) ++ le(self.columns, 2) ++ le(self.rows, 2)
/// its format: a BITMAPINFOHEADER for 24-bit MJPG frames
let streamFormat := le(40, 4) ++ le(self.columns, 4) ++ le(self.rows, 4) ++ le(1, 2) ++ le(24, 2) ++ "MJPG".encode() ++ le(self.columns * self.rows * 3, 4) ++ Bytes.filled(16, 0)
let headers := riffList("hdrl", riffChunk("avih", mainHeader) ++ riffList("strl", riffChunk("strh", streamHeader) ++ riffChunk("strf", streamFormat)))
let frames: Array<Bytes> := []
let index: Array<Bytes> := []
/// each frame's offset from the "movi" letters, where its chunk starts
let offset: U64 := 4
for jpeg in self.chunks {
let chunk := riffChunk("00dc", jpeg)
frames.push(chunk)
index.push("00dc".encode() ++ le(0x10, 4) ++ le(offset, 4) ++ le(jpeg.size(), 4))
set offset := offset + chunk.size()
}
let movie := riffList("movi", joined(frames))
let body := "AVI ".encode() ++ headers ++ movie ++ riffChunk("idx1", joined(index))
return "RIFF".encode() ++ uintBytes(body.size(), 4) ++ body
} uses #builtin.pcmDecode
/// the samples a format's data holds, as a Sound - or why it can't
local function soundOf(rate: U64, channels: U64, data: Bytes, offset: U64, count: U64, encoding: U64, format: String): ParseError ! Sound {
if channels == 0 then {
error ParseError.new(#format"not #{ format } sound this can read: it has no channels")
}
let samples := #builtin.pcmDecode(data, offset, count - count % channels, encoding)
if samples == undefined then {
error ParseError.new(#format"not #{ format } sound this can read: its samples are cut short")
}
return Sound{rate, channels, samples as Bytes}
}/// a bits value made one this format can write, rounded up
local function bitsOf(bits: U8, allowed: Array<U64>): U64 {
for b in allowed if (bits as U64) <= b {
return b
}
return allowed[allowed.size() - 1]!!
}/// PCM (8, 16, 24 and 32 bits), floats (32 and 64), A-law and mu-law - WAVE_FORMAT_EXTENSIBLE too
local function decodeWav(bytes: Bytes): ParseError ! Sound {
let size := bytes.size()
let format: U64 := 0
let channels: U64 := 0
let rate: U64 := 0
let bits: U64 := 0
let at: U64 := 12
for _ in Iterator.forever() while at + 8 <= size {
let kind := bytes.slice(at..<at + 4)!!.decode()
let length := uintAt(bytes, at + 4, 4)
let start := at + 8
if kind == "fmt " and start + 16 <= size then {
set format := uintAt(bytes, start, 2)
set channels := uintAt(bytes, start + 2, 2)
set rate := uintAt(bytes, start + 4, 4)
set bits := uintAt(bytes, start + 14, 2)
/// WAVE_FORMAT_EXTENSIBLE: the real format is its subformat GUID's first two bytes
if format == 0xFFFE and length >= 26 then {
set format := uintAt(bytes, start + 24, 2)
}
}
if kind == "data" then {
/// a streamed WAV's data has no length - the rest of the file
let end := if length == 0 or length == 0xFFFFFFFF or start + length > size then size else start + length
let encoding: U64? := if format == 1 then (if bits == 8 then EncodingUnsigned8 else if bits == 16 then EncodingInt16 else if bits == 24 then EncodingInt24 else if bits == 32 then EncodingInt32 else undefined) else if format == 3 then (if bits == 32 then EncodingFloat else if bits == 64 then EncodingDouble else undefined) else if format == 6 then EncodingALaw else if format == 7 then EncodingMuLaw else undefined
if encoding == undefined then {
error ParseError.new(#format"not WAV sound this can read: format #{ format } at #{ bits } bits a sample")
}
let width := if format == 6 or format == 7 then 1 else bits ÷ 8
return soundOf(rate, channels, bytes, start, (end - start) ÷ width, encoding as U64, "WAV")!!
}
set at := start + length + length % 2
}
error ParseError.new("not WAV sound this can read: it has no data")
} uses #builtin.bytesUint
/// an 80-bit IEEE extended float - a sample rate
local function extendedAt(bytes: Bytes, at: U64): F64 {
let exponent := uintAt(bytes, at, 2, true) band 0x7FFF
let mantissa := #builtin.bytesUint(bytes, at + 2, 4, true) * 4294967296.0 + #builtin.bytesUint(bytes, at + 6, 4, true)
if exponent == 0 and mantissa == 0.0 then {
return 0.0
}
return mantissa * 2.0 ^ ((exponent as F64) - 16383.0 - 63.0)
}/// a whole number (a sample rate) as an 80-bit IEEE extended float
local function extendedBytes(value: U64): Bytes {
if value == 0 then {
return Bytes.filled(10, 0)
}
/// the mantissa's top bit is the integer bit: shifted up until it's set
let shift: U64 := 0
for _ in Iterator.forever() while (value shr shift) > 1 {
set shift := shift + 1
}
let mantissa := (value as F64) * 2.0 ^ ((63 - shift) as F64)
let high := (mantissa / 4294967296.0).floor()
return uintBytes(16383 + shift, 2, true) ++ uintBytes(high as U64, 4, true) ++ uintBytes((mantissa - high * 4294967296.0) as U64, 4, true)
}/// AIFF (big-endian whole numbers) and AIFC (also little-endian ones, floats, A-law and mu-law)
local function decodeAiff(bytes: Bytes): ParseError ! Sound {
let size := bytes.size()
let channels: U64 := 0
let bits: U64 := 0
let rate: U64 := 0
let compression := "NONE"
let at: U64 := 12
for _ in Iterator.forever() while at + 8 <= size {
let kind := bytes.slice(at..<at + 4)!!.decode()
let length := uintAt(bytes, at + 4, 4, true)
let start := at + 8
if kind == "COMM" and start + 18 <= size then {
set channels := uintAt(bytes, start, 2, true)
set bits := uintAt(bytes, start + 6, 2, true)
set rate := extendedAt(bytes, start + 8).round() as U64
if length >= 22 then {
set compression := bytes.slice(start + 18..<start + 22)!!.decode()
}
}
if kind == "SSND" then {
let offset := uintAt(bytes, start, 4, true)
let data := start + 8 + offset
let end := (start + length).min(size)
let encoding: U64? := if compression == "NONE" or compression == "twos" then (if bits <= 8 then EncodingSigned8 else if bits <= 16 then EncodingInt16Big else if bits <= 24 then EncodingInt24Big else EncodingInt32Big) else if compression == "sowt" then (if bits <= 16 then EncodingInt16 else if bits <= 24 then EncodingInt24 else EncodingInt32) else if compression == "fl32" or compression == "FL32" then EncodingFloatBig else if compression == "fl64" or compression == "FL64" then EncodingDoubleBig else if compression == "alaw" or compression == "ALAW" then EncodingALaw else if compression == "ulaw" or compression == "ULAW" then EncodingMuLaw else undefined
if encoding == undefined then {
error ParseError.new(#format"not AIFF sound this can read: compression #{ compression }")
}
let width: U64 := ([1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8, 1, 1])[encoding as U64]!!
return soundOf(rate, channels, bytes, data, if end > data then (end - data) ÷ width else 0, encoding as U64, "AIFF")!!
}
set at := start + length + length % 2
}
error ParseError.new("not AIFF sound this can read: it has no sound data")
}/// mu-law, 8 to 32-bit whole numbers, floats, doubles and A-law - big-endian
local function decodeAu(bytes: Bytes): ParseError ! Sound {
if bytes.size() < 24 then {
error ParseError.new("not AU sound this can read: its header is cut short")
}
let offset := uintAt(bytes, 4, 4, true)
let length := uintAt(bytes, 8, 4, true)
let kind := uintAt(bytes, 12, 4, true)
let rate := uintAt(bytes, 16, 4, true)
let channels := uintAt(bytes, 20, 4, true)
let encodings: Array<U64> := [99, EncodingMuLaw, EncodingSigned8, EncodingInt16Big, EncodingInt24Big, EncodingInt32Big, EncodingFloatBig, EncodingDoubleBig]
let encoding := if kind == 27 then EncodingALaw else if kind >= 1 and kind <= 7 then encodings[kind]!! else 99
if encoding == 99 then {
error ParseError.new(#format"not AU sound this can read: encoding #{ kind }")
}
let width: U64 := ([1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8, 1, 1])[encoding]!!
let end := if length == 0xFFFFFFFF or offset + length > bytes.size() then bytes.size() else offset + length
return soundOf(rate, channels, bytes, offset, if end > offset then (end - offset) ÷ width else 0, encoding, "AU")!!
} uses #builtin.flacDecode
/// STREAMINFO's rate, channels, bits a sample and total samples, then every frame
local function decodeFlac(bytes: Bytes): ParseError ! Sound {
let size := bytes.size()
let at: U64 := 4
let rate: U64 := 0
let channels: U64 := 0
let bits: U64 := 0
let total: U64 := 0
for _ in Iterator.forever() while at + 4 <= size {
let header := uintAt(bytes, at, 1)
let length := uintAt(bytes, at + 1, 3, true)
if header band 0x7F == 0 and length >= 34 then {
/// 20 bits of rate, 3 of channels less 1, 5 of bits less 1, 36 of total samples
let packed := at + 4 + 10
set rate := uintAt(bytes, packed, 3, true) shr 4
set channels := ((uintAt(bytes, packed + 2, 1) shr 1) band 7) + 1
set bits := (((uintAt(bytes, packed + 2, 1) band 1) shl 4) bor (uintAt(bytes, packed + 3, 1) shr 4)) + 1
set total := ((uintAt(bytes, packed + 3, 1) band 15) shl 32) + uintAt(bytes, packed + 4, 4, true)
}
set at := at + 4 + length
if header band 0x80 != 0 then {
break _
}
}
if channels == 0 or rate == 0 then {
error ParseError.new("not FLAC sound this can read: it has no STREAMINFO")
}
let samples := #builtin.flacDecode(bytes, at, channels, bits, total)
if samples == undefined then {
error ParseError.new("not FLAC sound this can read: its frames are broken")
}
return Sound{rate, channels, samples as Bytes}
}@async
local function retryAfter<Type>(left: U64, start: Lambda<(), Promise<Type>>, wait: Duration): Promise<Type> {
let value := await start()
if not value.is<Error>() or left <= 1 or &cancellation.isCancelled() then { return value }
await Promise.sleep(wait)
if &cancellation.isCancelled() then { return value }
return await retryAfter(left - 1, start, wait.scale(2.0))
}#unknownglobal method is for Unknown<Target>(self: Unknown): Boolean {
return Target.isAssignableFrom(self.typeOf())
}global method narrow for Unknown<Target>(self: Unknown): CastError ! Target {
if not self.is<Target>() then { error CastError.new("value is a " ++ self.typeOf().name() ++ ", not a " ++ Target.name()) }
return self as Target
} uses #runtime.typeof
global method typeOf for Unknown(self: Unknown): Type {
return #runtime.typeof(self)
}#neverno values, so nothing to implement
No members of its own.
#undefinedNo members of its own.
#equatable-for-undefinedmethod equals(self: Undefined, other: Undefined): Boolean {
return true
}#hashable-for-undefinedmethod hash(self: Undefined): U64 {
return &hashSeed
}#tostring-for-undefinedmethod toString(self: Undefined): String {
return "undefined"
}#default-for-undefinedfunction default(): Undefined {
return undefined
}#copy-for-undefinedNothing to write - a marker interface, or one another block already covers.
#typetype metadata - every member reads the compiler's description of the type
uses #comptime.typeName
/// "Array<U8>"
global method name for Type(self: Type): String {
return #comptime.typeName(self)
}global method generics for Type(self: Type): Array<Type> {
return #comptime.typeGenerics(self)
} uses #comptime.implements
global method implements for Type(self: Type, interface: Interface): Boolean {
return #comptime.implements(self, interface)
}global method isAssignableFrom for Type(self: Type, other: Type): Boolean {
return #comptime.isAssignable(self, other)
} uses #comptime.typeFields
global method fields for Type(self: Type): Map<String, Type> {
return #comptime.typeFields(self)
}global method variants for Type(self: Type): Array<Type> {
return #comptime.typeVariants(self)
} uses #comptime.sizeOf
global method size for Type(self: Type): U64 {
return #comptime.sizeOf(self)
}global function of for Type(value: Unknown): Type {
return value.typeOf()
} uses #comptime.typeNamed
/// the type written as name - "Point", "Shape.Circle" - found when it's used, like a value's own type
global function named for Type(name: String): Type {
return #comptime.typeNamed(name)
}#equatable-for-type uses #comptime.sameType
method equals(self: Type, other: Type): Boolean {
return #comptime.sameType(self, other)
}#hashable-for-typemethod hash(self: Type): U64 {
return self.name().hash()
}#tostring-for-typemethod toString(self: Type): String {
return self.name()
}#booleanglobal method implies for Boolean(self: Boolean, other: Boolean): Boolean {
return not self or other
}global method toTrilean for Boolean(self: Boolean): Trilean {
return self ? true : false
}#equatable-for-boolean uses #builtin.intrinsic
method equals(self: Boolean, other: Boolean): Boolean {
return #builtin.intrinsic.EQ_BOOLEAN_BOOLEAN(self, other)
}#hashable-for-booleanmethod hash(self: Boolean): U64 {
return hashCombine(&hashSeed, self ? 1 : 0)
}#comparable-for-boolean uses #builtin.intrinsic
method compare(self: Boolean, other: Boolean): Ordering {
return #builtin.intrinsic.CMP_BOOLEAN_BOOLEAN(self, other)
}#tostring-for-booleanmethod toString(self: Boolean): String {
return self ? "true" : "false"
}#fromstring-for-booleanfunction parse(text: String): ParseError ! Boolean {
if text == "true" then { return true }
if text == "false" then { return false }
error ParseError.new("expected \"true\" or \"false\", got \"" ++ text ++ "\"")
}#logical-for-boolean uses #builtin.intrinsic
method not(self: Boolean): Boolean {
return #builtin.intrinsic.NOT_BOOLEAN(self)
} uses #builtin.intrinsic
/// the operator only evaluates b when a is true - this method is what it calls once it has both
method and(self: Boolean, other: Boolean): Boolean {
return #builtin.intrinsic.AND_BOOLEAN_BOOLEAN(self, other)
} uses #builtin.intrinsic
/// the operator only evaluates b when a is false
method or(self: Boolean, other: Boolean): Boolean {
return #builtin.intrinsic.OR_BOOLEAN_BOOLEAN(self, other)
} uses #builtin.intrinsic
method xor(self: Boolean, other: Boolean): Boolean {
return #builtin.intrinsic.XOR_BOOLEAN_BOOLEAN(self, other)
} uses #builtin.intrinsic
method iff(self: Boolean, other: Boolean): Boolean {
return #builtin.intrinsic.IFF_BOOLEAN_BOOLEAN(self, other)
}#default-for-booleanfunction default(): Boolean {
return false
}#copy-for-booleanNothing to write - a marker interface, or one another block already covers.
#trileanKleene's three-valued logic - null is "unknown"
global method isKnown for Trilean(self: Trilean): Boolean {
return self != null
}global method toBoolean for Trilean(self: Trilean): Boolean? {
return self == null ? undefined : self == true
}global method orElse for Trilean(self: Trilean, fallback: Boolean): Boolean {
return self.toBoolean() ?? fallback
}#equatable-for-trilean/// both the same Boolean, or both null
method equals(self: Trilean, other: Trilean): Boolean {
if self is Boolean and other is Boolean then { return (self as Boolean).equals(other as Boolean) }
return not (self is Boolean) and not (other is Boolean)
}#hashable-for-trileanmethod hash(self: Trilean): U64 {
return hashCombine(&hashSeed, self == null ? 2 : self == true ? 1 : 0)
}#tostring-for-trileanmethod toString(self: Trilean): String {
return self == null ? "null" : self == true ? "true" : "false"
}#fromstring-for-trileanfunction parse(text: String): ParseError ! Trilean {
if text == "null" then { return null }
return Boolean.parse(text)!!
}#logical-for-trileanmethod not(self: Trilean): Trilean {
return self == null ? null : self == false
}method and(self: Trilean, other: Trilean): Trilean {
if self == false or other == false then { return false }
if self == null or other == null then { return null }
return true
}method or(self: Trilean, other: Trilean): Trilean {
if self == true or other == true then { return true }
if self == null or other == null then { return null }
return false
}method xor(self: Trilean, other: Trilean): Trilean {
if self == null or other == null then { return null }
return self != other
}method iff(self: Trilean, other: Trilean): Trilean {
if self == null or other == null then { return null }
return self == other
}#default-for-trileanfunction default(): Trilean {
return null
}#copy-for-trileanNothing to write - a marker interface, or one another block already covers.
#orderingglobal method reverse for Ordering(self: Ordering): Ordering {
match self {
case Ordering.Less then { return Ordering.Greater }
case Ordering.Greater then { return Ordering.Less }
case _ then { return Ordering.Equal }
}
}global method then for Ordering(self: Ordering, other: Ordering): Ordering {
return self == Ordering.Equal ? other : self
}global method thenBy for Ordering(self: Ordering, next: Lambda<(), Ordering>): Ordering {
return self == Ordering.Equal ? next() : self
}global method isLess for Ordering(self: Ordering): Boolean {
return self == Ordering.Less
}global method isEqual for Ordering(self: Ordering): Boolean {
return self == Ordering.Equal
}global method isGreater for Ordering(self: Ordering): Boolean {
return self == Ordering.Greater
}global method toInteger for Ordering(self: Ordering): S8 {
return self.isLess() ? -1 : self.isGreater() ? 1 : 0
}global function of for Ordering<T: Comparable>(a: T, b: T): Ordering {
return a.compare(b)
}#equatable-for-orderingmethod equals(self: Ordering, other: Ordering): Boolean {
return self.index() == other.index()
}#hashable-for-orderingmethod hash(self: Ordering): U64 {
return hashCombine(&hashSeed, self.index())
}#comparable-for-orderingmethod compare(self: Ordering, other: Ordering): Ordering {
return self.index().compare(other.index())
}#tostring-for-orderingmethod toString(self: Ordering): String {
return self.variantName()
}#copy-for-orderingNothing to write - a marker interface, or one another block already covers.
#u8-u16-u32-u64-u128-u256-s8-s16-s32-s64-s128-s256written once for IntegerTypes - one copy per integer type
local function isSigned for IntegerTypes(): Boolean {
return Self.name().startsWith("S")
}/// a U16 - U256's 256 doesn't fit a U8
global function bits for IntegerTypes(): U16 {
return (Self.size() * 8) as U16
}/// a shl / shr count brought down to one that fits in Self and means the same - bits itself at most,
/// which the intrinsic shifts every bit out by
local function shiftCount for IntegerTypes(count: U64): Self {
return count.min(Self.bits() as U64) as Self
} uses #builtin.intrinsic
global method abs for IntegerTypes(self: Self): Self {
return #builtin.intrinsic.ABS_Self(self)
}global method sign for IntegerTypes(self: Self): S8 {
return self > 0 ? 1 : self < 0 ? -1 : 0
}global method isZero for IntegerTypes(self: Self): Boolean {
return self == 0
}global method isPositive for IntegerTypes(self: Self): Boolean {
return self > 0
}global method isNegative for IntegerTypes(self: Self): Boolean {
return self < 0
}global method isEven for IntegerTypes(self: Self): Boolean {
return (self band 1) == 0
}global method isOdd for IntegerTypes(self: Self): Boolean {
return (self band 1) == 1
} uses #builtin.intrinsic
global method min for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MIN_Self_Self(self, other)
} uses #builtin.intrinsic
global method max for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MAX_Self_Self(self, other)
}global method clamp for IntegerTypes(self: Self, range: Range<Self>): Self {
return range.clamp(self)
}/// square-and-multiply - log2(exponent) multiplications, an OverflowError when one doesn't fit
global method pow for IntegerTypes(self: Self, exponent: U32): Self {
let result: Self := 1
let base := self
let remaining := exponent
for _ in forever() while remaining > 0 {
if remaining.isOdd() then { set result *= base }
set remaining := remaining shr 1
if remaining > 0 then { set base *= base }
}
return result
}/// Newton's method from a power of two at or above the root - converges downward
global method sqrt for IntegerTypes(self: Self): CastError ! Self {
if self < 0 then { error CastError.new("no integer square root of " ++ self.toString()) }
if self < 2 then { return self }
let used := Self.bits() - (self.leadingZeros() as U16)
let x := (1 as Self).shl(((used + 1) / 2) as U64)
for _ in forever() {
let y := (x + self / x) / 2
if y >= x then { return x }
set x := y
}
}/// Euclid's algorithm - always non-negative
global method gcd for IntegerTypes(self: Self, other: Self): Self {
let a := self.abs()
let b := other.abs()
for _ in forever() while b != 0 {
let rest := a % b
set a := b
set b := rest
}
return a
}global method lcm for IntegerTypes(self: Self, other: Self): Self {
if self == 0 or other == 0 then { return 0 }
return (self / self.gcd(other) * other).abs()
}global method divMod for IntegerTypes(self: Self, other: Self): (Self, Self) {
return (self // other, self % other)
} uses #builtin.intrinsic
global method modulo for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MOD_Self_Self(self, other)
} uses #builtin.intrinsic
global method checkedAdd for IntegerTypes(self: Self, other: Self): OverflowError ! Self {
return #builtin.intrinsic.ADD_CHECKED_Self_Self(self, other)
} uses #builtin.intrinsic
global method checkedSubtract for IntegerTypes(self: Self, other: Self): OverflowError ! Self {
return #builtin.intrinsic.SUB_CHECKED_Self_Self(self, other)
} uses #builtin.intrinsic
global method checkedMultiply for IntegerTypes(self: Self, other: Self): OverflowError ! Self {
return #builtin.intrinsic.MUL_CHECKED_Self_Self(self, other)
} uses #builtin.intrinsic
global method wrappingAdd for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_WRAPPING_Self_Self(self, other)
} uses #builtin.intrinsic
global method wrappingSubtract for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_WRAPPING_Self_Self(self, other)
} uses #builtin.intrinsic
global method wrappingMultiply for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_WRAPPING_Self_Self(self, other)
} uses #builtin.intrinsic
global method saturatingAdd for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_SATURATING_Self_Self(self, other)
} uses #builtin.intrinsic
global method saturatingSubtract for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_SATURATING_Self_Self(self, other)
} uses #builtin.intrinsic
global method saturatingMultiply for IntegerTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_SATURATING_Self_Self(self, other)
} uses #builtin.intrinsic
global method rotateLeft for IntegerTypes(self: Self, count: U8): Self {
return #builtin.intrinsic.ROTL_Self_Self(self, ((count as U16) % Self.bits()) as Self)
} uses #builtin.intrinsic
global method rotateRight for IntegerTypes(self: Self, count: U8): Self {
return #builtin.intrinsic.ROTR_Self_Self(self, ((count as U16) % Self.bits()) as Self)
} uses #builtin.intrinsic
global method countOnes for IntegerTypes(self: Self): U8 {
return #builtin.intrinsic.POPCNT_Self(self)
}global method countZeros for IntegerTypes(self: Self): U8 {
return (Self.bits() - (self.countOnes() as U16)) as U8
} uses #builtin.intrinsic
global method leadingZeros for IntegerTypes(self: Self): U8 {
return #builtin.intrinsic.CLZ_Self(self)
} uses #builtin.intrinsic
global method trailingZeros for IntegerTypes(self: Self): U8 {
return #builtin.intrinsic.CTZ_Self(self)
}global method isPowerOfTwo for IntegerTypes(self: Self): Boolean {
return self > 0 and (self band (self - 1)) == 0
}global method bit for IntegerTypes(self: Self, index: U8): Boolean {
return (self.shr(index as U64) band 1) == 1
}global method withBit for IntegerTypes(self: Self, index: U8, value: Boolean): Self {
let mask := (1 as Self).shl(index as U64)
return value ? self bor mask : self band ~mask
}/// a signed value's bytes come from it as an S256 - 0xFF doesn't fit an S8, and its shr keeps the sign
global method toBytes for IntegerTypes(self: Self): Bytes {
let count := (Self.bits() / 8) as U64
let bytes := Bytes.filled(count)
for i in 0..<count {
set bytes[i] := Self.isSigned() ? ((self as S256).shr(i * 8) band 0xFF) as U8 : (self.shr(i * 8) band 0xFF) as U8
}
return bytes
}/// two's complement for S* - the top byte's high bit counts negative
global function fromBytes for IntegerTypes(bytes: Bytes): ParseError ! Self {
let count := (Self.bits() / 8) as U64
if bytes.size() != count then { error ParseError.new("a " ++ Self.name() ++ " needs exactly " ++ count.toString() ++ " bytes") }
if Self.isSigned() then {
let signed: S256 := 0
for byte, i in bytes {
set signed := signed + ((byte as S256) - (i + 1 == count and byte >= 128 ? 256 : 0)).shl(i * 8)
}
return signed as Self
}
let result: Self := 0
for byte, i in bytes {
set result := result bor (byte as Self).shl(i * 8)
}
return result
}global method toFloat for IntegerTypes(self: Self): Float {
return self as Float
}global method toBigInteger for IntegerTypes(self: Self): BigInteger {
return BigInteger.parse(self.toString())!!
}global method toCodepoint for IntegerTypes(self: Self): CastError ! Codepoint {
return Codepoint.fromU32(self.checkedCast<U32>()!!)
}/// `as`, with a CastError where it gives an OverflowError
global method checkedCast for IntegerTypes<Target>(self: Self): CastError ! Target {
match self as Target {
case Error then { error CastError.new(self.toString() ++ " doesn't fit in a " ++ Target.name()) }
case result then { return result }
}
}#equatable-for-integertypes uses #builtin.intrinsic
method equals(self: Self, other: Self): Boolean {
return #builtin.intrinsic.EQ_Self_Self(self, other)
}#hashable-for-integertypesmethod hash(self: Self): U64 {
return hashBytes(self.toBytes())
}#comparable-for-integertypes uses #builtin.intrinsic
method compare(self: Self, other: Self): Ordering {
return #builtin.intrinsic.CMP_Self_Self(self, other)
}#bounded-for-integertypes/// the sign bit alone for S*, 0 for U*
function min(): Self {
return Self.isSigned() ? (1 as Self).shl((Self.bits() - 1) as U64) : 0
}/// every bit but the sign bit for S*, every bit for U*
function max(): Self {
return ~Self.min()
}#tostring-for-integertypes/// digits come out least significant first; a negative value divides toward zero, so the
/// remainders are negative too and abs() fixes each one - S*.min() is never negated whole
method toString(self: Self, radix: U8 := 10): String {
if self == 0 then { return "0" }
let base := radix as Self
let digits: Array<Codepoint> := []
let rest := self
for _ in forever() while rest != 0 {
digits.push(digitChar((rest % base).abs() as U8))
set rest := rest / base
}
if self < 0 then { digits.push('-') }
return String.fromCodepoints(digits.reverse())
}#fromstring-for-integertypes/// accumulates toward the sign it ends with, so S*.min() parses without overflowing
function parse(text: String, radix: U8 := 10): ParseError ! Self {
if radix < 2 or radix > 36 then { error ParseError.new("radix must be from 2 to 36") }
let trimmed := text.trim().replaceAll("_", "")
let negative := trimmed.startsWith("-")
let digits := trimmed.stripPrefix("-") ?? trimmed.stripPrefix("+") ?? trimmed
if digits.isEmpty() or (negative and not Self.isSigned()) then { error ParseError.new("not a " ++ Self.name() ++ ": " ++ text) }
let base := radix as Self
let result: Self := 0
for c in digits {
let digit := ((c.digitValue(radix) ?! ParseError.new("not a base-" ++ radix.toString() ++ " digit: " ++ c.toString()))!!) as Self
let shifted := result.checkedMultiply(base) !? undefined
let next := negative ? shifted?.checkedSubtract(digit) : shifted?.checkedAdd(digit)
set result := ((next ?? undefined) ?! ParseError.new(text ++ " doesn't fit in a " ++ Self.name()))!!
}
return result
}#numeric-for-integertypesfunction zero(): Self {
return 0
}function one(): Self {
return 1
} uses #builtin.intrinsic
method add(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_Self_Self(self, other)
} uses #builtin.intrinsic
method subtract(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_Self_Self(self, other)
} uses #builtin.intrinsic
method multiply(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_Self_Self(self, other)
} uses #builtin.intrinsic
method divide(self: Self, other: Self): Self {
return #builtin.intrinsic.DIV_Self_Self(self, other)
} uses #builtin.intrinsic
method floorDivide(self: Self, other: Self): Self {
return #builtin.intrinsic.FLOORDIV_Self_Self(self, other)
} uses #builtin.intrinsic
method remainder(self: Self, other: Self): Self {
return #builtin.intrinsic.REM_Self_Self(self, other)
} uses #builtin.intrinsic
method power(self: Self, exponent: Self): Self {
return #builtin.intrinsic.POW_Self_Self(self, exponent)
} uses #builtin.intrinsic
method negate(self: Self): Self {
return #builtin.intrinsic.NEG_Self(self)
}method plus(self: Self): Self {
return self
}#bitwise-for-integertypes uses #builtin.intrinsic
method bor(self: Self, other: Self): Self {
return #builtin.intrinsic.BOR_Self_Self(self, other)
} uses #builtin.intrinsic
method bxor(self: Self, other: Self): Self {
return #builtin.intrinsic.BXOR_Self_Self(self, other)
} uses #builtin.intrinsic
method band(self: Self, other: Self): Self {
return #builtin.intrinsic.BAND_Self_Self(self, other)
} uses #builtin.intrinsic
method bnot(self: Self): Self {
return #builtin.intrinsic.BNOT_Self(self)
} uses #builtin.intrinsic
method shl(self: Self, count: U64): Self {
return #builtin.intrinsic.SHL_Self_Self(self, Self.shiftCount(count))
} uses #builtin.intrinsic
method shr(self: Self, count: U64): Self {
return #builtin.intrinsic.SHR_Self_Self(self, Self.shiftCount(count))
}#steppable-for-integertypesmethod next(self: Self): Self {
return self + 1
}method previous(self: Self): Self {
return self - 1
}#castable-target-for-integertypesone copy per pair of types - Target is every other number type, and casting to Self itself is self
uses #builtin.intrinsic
method cast(self: Self): Target {
if Target == Self then { return self }
return #builtin.intrinsic.CAST_Self_Target(self)
}#default-for-integertypesfunction default(): Self {
return 0
}#copy-for-integertypesNothing to write - a marker interface, or one another block already covers.
#f16-f32-f64written once for FloatTypes; Bits is the unsigned integer of the same width (U16, U32, U64)
/// stored mantissa bits, and the largest exponent - IEEE-754 binary16, binary32, binary64
local function mantissaBits for FloatTypes(): U8 {
return Self == F16 ? 10 : Self == F32 ? 23 : 52
}local function maxExponent for FloatTypes(): U16 {
return Self == F16 ? 15 : Self == F32 ? 127 : 1023
}global function epsilon for FloatTypes(): Self {
return (2 as Self).power(-(Self.mantissaBits() as Self))
}global function infinity for FloatTypes(): Self {
return 1.0 / 0.0
}global function nan for FloatTypes(): Self {
return 0.0 / 0.0
}/// more digits than F64 holds - the literal rounds to Self
global function pi for FloatTypes(): Self {
return 3.141592653589793238462643383279502884197169399375105820974944592307816406286
}global function e for FloatTypes(): Self {
return 2.718281828459045235360287471352662497757247093699959574966967627724076630354
}global function tau for FloatTypes(): Self {
return 2 * Self.pi()
} uses #builtin.intrinsic
global method abs for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ABS_Self(self)
}global method sign for FloatTypes(self: Self): Self {
if self.isNaN() then { return self }
return self > 0 ? 1.0 : self < 0 ? -1.0 : 0.0
} uses #builtin.intrinsic
global method isNaN for FloatTypes(self: Self): Boolean {
return #builtin.intrinsic.ISNAN_Self(self)
} uses #builtin.intrinsic
global method isInfinite for FloatTypes(self: Self): Boolean {
return #builtin.intrinsic.ISINF_Self(self)
}global method isFinite for FloatTypes(self: Self): Boolean {
return not self.isNaN() and not self.isInfinite()
}global method isInteger for FloatTypes(self: Self): Boolean {
return self.isFinite() and self == self.truncate()
} uses #builtin.intrinsic
global method min for FloatTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MIN_Self_Self(self, other)
} uses #builtin.intrinsic
global method max for FloatTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.MAX_Self_Self(self, other)
}global method clamp for FloatTypes(self: Self, range: Range<Self>): Self {
return range.clamp(self)
}global method approxEquals for FloatTypes(self: Self, other: Self, tolerance: Self := Self.epsilon()): Boolean {
return (self - other).abs() <= tolerance
} uses #builtin.intrinsic
global method floor for FloatTypes(self: Self): Self {
return #builtin.intrinsic.FLOOR_Self(self)
} uses #builtin.intrinsic
global method ceil for FloatTypes(self: Self): Self {
return #builtin.intrinsic.CEIL_Self(self)
} uses #builtin.intrinsic
global method round for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ROUND_Self(self)
} uses #builtin.intrinsic
global method roundHalfEven for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ROUNDEVEN_Self(self)
} uses #builtin.intrinsic
global method truncate for FloatTypes(self: Self): Self {
return #builtin.intrinsic.TRUNC_Self(self)
}global method fraction for FloatTypes(self: Self): Self {
return self - self.truncate()
}global method roundTo for FloatTypes(self: Self, digits: U8): Self {
let factor := (10 as Self).power(digits as Self)
return (self * factor).round() / factor
} uses #builtin.intrinsic
global method sqrt for FloatTypes(self: Self): Self {
return #builtin.intrinsic.SQRT_Self(self)
} uses #builtin.intrinsic
global method cbrt for FloatTypes(self: Self): Self {
return #builtin.intrinsic.CBRT_Self(self)
} uses #builtin.intrinsic
global method exp for FloatTypes(self: Self): Self {
return #builtin.intrinsic.EXP_Self(self)
} uses #builtin.intrinsic
global method ln for FloatTypes(self: Self): Self {
return #builtin.intrinsic.LN_Self(self)
}global method log for FloatTypes(self: Self, base: Self): Self {
return self.ln() / base.ln()
} uses #builtin.intrinsic
global method log2 for FloatTypes(self: Self): Self {
return #builtin.intrinsic.LOG2_Self(self)
} uses #builtin.intrinsic
global method log10 for FloatTypes(self: Self): Self {
return #builtin.intrinsic.LOG10_Self(self)
} uses #builtin.intrinsic
global method hypot for FloatTypes(self: Self, other: Self): Self {
return #builtin.intrinsic.HYPOT_Self_Self(self, other)
} uses #builtin.intrinsic
global method sin for FloatTypes(self: Self): Self {
return #builtin.intrinsic.SIN_Self(self)
} uses #builtin.intrinsic
global method cos for FloatTypes(self: Self): Self {
return #builtin.intrinsic.COS_Self(self)
} uses #builtin.intrinsic
global method tan for FloatTypes(self: Self): Self {
return #builtin.intrinsic.TAN_Self(self)
} uses #builtin.intrinsic
global method asin for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ASIN_Self(self)
} uses #builtin.intrinsic
global method acos for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ACOS_Self(self)
} uses #builtin.intrinsic
global method atan for FloatTypes(self: Self): Self {
return #builtin.intrinsic.ATAN_Self(self)
} uses #builtin.intrinsic
global method atan2 for FloatTypes(self: Self, x: Self): Self {
return #builtin.intrinsic.ATAN2_Self_Self(self, x)
} uses #builtin.intrinsic
global method sinh for FloatTypes(self: Self): Self {
return #builtin.intrinsic.SINH_Self(self)
} uses #builtin.intrinsic
global method cosh for FloatTypes(self: Self): Self {
return #builtin.intrinsic.COSH_Self(self)
} uses #builtin.intrinsic
global method tanh for FloatTypes(self: Self): Self {
return #builtin.intrinsic.TANH_Self(self)
}global method toRadians for FloatTypes(self: Self): Self {
return self * Self.pi() / 180
}global method toDegrees for FloatTypes(self: Self): Self {
return self * 180 / Self.pi()
} uses #builtin.intrinsic
/// fused, so t = 1 lands exactly on to
global method lerp for FloatTypes(self: Self, to: Self, t: Self): Self {
return #builtin.intrinsic.FMA_Self_Self_Self(to - self, t, self)
} uses #builtin.intrinsic
global method toInteger for FloatTypes(self: Self): CastError ! Integer {
return #builtin.intrinsic.CAST_Self_S64(self)
}/// the sign bit is cleared by abs() alone, so comparing bits tells -0.0 from 0.0 - which < can't
global method ulpDistance for FloatTypes(self: Self, other: Self): U256? {
if self.isNaN() or other.isNaN() then { return undefined }
let a := self.abs().toBits() as U256
let b := other.abs().toBits() as U256
let sameSign := (self.toBits() == self.abs().toBits()) == (other.toBits() == other.abs().toBits())
return sameSign ? (a > b ? a - b : b - a) : a + b
} uses #builtin.intrinsic
global method toBits for FloatTypes(self: Self): Bits {
return #builtin.intrinsic.BITCAST_Self_Bits(self)
} uses #builtin.intrinsic
global function fromBits for FloatTypes(bits: Bits): Self {
return #builtin.intrinsic.BITCAST_Bits_Self(bits)
}#equatable-for-floattypes uses #builtin.intrinsic
/// IEEE-754 equality - NaN equals nothing, 0.0 equals -0.0
method equals(self: Self, other: Self): Boolean {
return #builtin.intrinsic.EQ_Self_Self(self, other)
}#hashable-for-floattypes/// equal values hash the same: only 0.0 and -0.0 are equal without being the same bits, so they share one
method hash(self: Self): U64 {
let normalized := self == 0 ? 0.0 : self
return hashBytes(normalized.toBits().toBytes())
}#comparable-for-floattypes uses #builtin.intrinsic
/// a total order - NaN after everything, and Equal for == values, like the intrinsic
method compare(self: Self, other: Self): Ordering {
return #builtin.intrinsic.CMP_Self_Self(self, other)
}#bounded-for-floattypesfunction min(): Self {
return -Self.max()
}/// the largest mantissa at the largest exponent - (2 - epsilon) * 2^maxExponent
function max(): Self {
return (2 - Self.epsilon()) * (2 as Self).power(Self.maxExponent() as Self)
}#tostring-for-floattypes uses #builtin.formatFloat
/// "NaN", "Infinity" and "-Infinity" for the special values
method toString(self: Self, precision: U8?): String {
return #builtin.formatFloat(self, precision)
}#fromstring-for-floattypes uses #builtin.parseFloat
function parse(text: String): ParseError ! Self {
return #builtin.parseFloat<Self>(text)
}#numeric-for-floattypesfunction zero(): Self {
return 0.0
}function one(): Self {
return 1.0
} uses #builtin.intrinsic
method add(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_Self_Self(self, other)
} uses #builtin.intrinsic
method subtract(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_Self_Self(self, other)
} uses #builtin.intrinsic
method multiply(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_Self_Self(self, other)
} uses #builtin.intrinsic
method divide(self: Self, other: Self): Self {
return #builtin.intrinsic.DIV_Self_Self(self, other)
} uses #builtin.intrinsic
method floorDivide(self: Self, other: Self): Self {
return #builtin.intrinsic.FLOORDIV_Self_Self(self, other)
} uses #builtin.intrinsic
method remainder(self: Self, other: Self): Self {
return #builtin.intrinsic.REM_Self_Self(self, other)
} uses #builtin.intrinsic
method power(self: Self, exponent: Self): Self {
return #builtin.intrinsic.POW_Self_Self(self, exponent)
} uses #builtin.intrinsic
method negate(self: Self): Self {
return #builtin.intrinsic.NEG_Self(self)
}method plus(self: Self): Self {
return self
}#castable-target-for-floattypesone copy per pair of types - Target is every other number type, and casting to Self itself is self
uses #builtin.intrinsic
method cast(self: Self): Target {
if Target == Self then { return self }
return #builtin.intrinsic.CAST_Self_Target(self)
}#default-for-floattypesfunction default(): Self {
return 0.0
}#copy-for-floattypesNothing to write - a marker interface, or one another block already covers.
#c32-c64written once for ComplexTypes; Part is F32, F64 respectively
Layout
global function new for ComplexTypes(real: Part, imaginary: Part): Self {
return Self{realPart := real, imaginaryPart := imaginary}
}global function fromPolar for ComplexTypes(magnitude: Part, angle: Part): Self {
return Self.new(magnitude * angle.cos(), magnitude * angle.sin())
}global function i for ComplexTypes(): Self {
return Self.new(0, 1)
}global method real for ComplexTypes(self: Self): Part {
return self.realPart
}global method imaginary for ComplexTypes(self: Self): Part {
return self.imaginaryPart
}global method magnitude for ComplexTypes(self: Self): Part {
return self.real().hypot(self.imaginary())
}global method magnitudeSquared for ComplexTypes(self: Self): Part {
return self.real() * self.real() + self.imaginary() * self.imaginary()
}global method argument for ComplexTypes(self: Self): Part {
return self.imaginary().atan2(self.real())
}global method toPolar for ComplexTypes(self: Self): (Part, Part) {
return (self.magnitude(), self.argument())
}global method conjugate for ComplexTypes(self: Self): Self {
return Self.new(self.real(), -self.imaginary())
}global method reciprocal for ComplexTypes(self: Self): Self {
return self.conjugate().scale(1 / self.magnitudeSquared())
}/// the principal root - half the angle, the root of the magnitude
global method sqrt for ComplexTypes(self: Self): Self {
return Self.fromPolar(self.magnitude().sqrt(), self.argument() / 2)
}global method exp for ComplexTypes(self: Self): Self {
return Self.fromPolar(self.real().exp(), self.imaginary())
}global method ln for ComplexTypes(self: Self): Self {
return Self.new(self.magnitude().ln(), self.argument())
}global method isReal for ComplexTypes(self: Self): Boolean {
return self.imaginary() == 0
}global method isNaN for ComplexTypes(self: Self): Boolean {
return self.real().isNaN() or self.imaginary().isNaN()
}global method approxEquals for ComplexTypes(self: Self, other: Self, tolerance: Part := Part.epsilon()): Boolean {
return (self - other).magnitude() <= tolerance
}#equatable-for-complextypes uses #builtin.intrinsic
method equals(self: Self, other: Self): Boolean {
return #builtin.intrinsic.EQ_Self_Self(self, other)
}#hashable-for-complextypesmethod hash(self: Self): U64 {
return hashCombine(self.real().hash(), self.imaginary().hash())
}#tostring-for-complextypes/// "1+2i", "1-2i"
method toString(self: Self): String {
let sign := self.imaginary() < 0 ? "-" : "+"
return self.real().toString() ++ sign ++ self.imaginary().abs().toString() ++ "i"
}#fromstring-for-complextypes/// "a", "bi", "a+bi", "a-bi" - the sign between the parts is what splits them
function parse(text: String): ParseError ! Self {
let trimmed := text.trim().replaceAll(" ", "")
if not trimmed.endsWith("i") then { return Self.new(Part.parse(trimmed)!!, 0) }
let body := trimmed.slice(0..<(trimmed.size() - 1)) !? ""
let split := body.lastIndexOf(Regex("(?<=[0-9.])[+-]")!!)
if split == undefined then { return Self.new(0, body == "" or body == "+" ? 1 : body == "-" ? -1 : Part.parse(body)!!) }
let real := Part.parse(body.slice(0..<split) !? "")!!
let rest := body.slice(split..<body.size()) !? ""
let imaginary := rest == "+" ? 1 : rest == "-" ? -1 : Part.parse(rest)!!
return Self.new(real, imaginary)
}#numeric-for-complextypesfunction zero(): Self {
return Self.new(0, 0)
}function one(): Self {
return Self.new(1, 0)
} uses #builtin.intrinsic
method add(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_Self_Self(self, other)
} uses #builtin.intrinsic
method subtract(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_Self_Self(self, other)
} uses #builtin.intrinsic
method multiply(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_Self_Self(self, other)
} uses #builtin.intrinsic
method divide(self: Self, other: Self): Self {
return #builtin.intrinsic.DIV_Self_Self(self, other)
}/// floors each part of the quotient
method floorDivide(self: Self, other: Self): Self {
let quotient := self / other
return Self.new(quotient.real().floor(), quotient.imaginary().floor())
}method remainder(self: Self, other: Self): Self {
return self - other * self.floorDivide(other)
} uses #builtin.intrinsic
method power(self: Self, exponent: Self): Self {
return #builtin.intrinsic.POW_Self_Self(self, exponent)
} uses #builtin.intrinsic
method negate(self: Self): Self {
return #builtin.intrinsic.NEG_Self(self)
}method plus(self: Self): Self {
return self
}#scalable-part-for-complextypesmethod scale(self: Self, factor: Part): Self {
return Self.new(self.real() * factor, self.imaginary() * factor)
}#castable-target-for-complextypesone copy per pair of complex types; casting to Self itself is self
uses #builtin.intrinsic
method cast(self: Self): Target {
if Target == Self then { return self }
return #builtin.intrinsic.CAST_Self_Target(self)
}#default-for-complextypesfunction default(): Self {
return Self.new(0, 0)
}#copy-for-complextypesNothing to write - a marker interface, or one another block already covers.
#q32-q64written once for QuaternionTypes; Part is F32, F64 respectively
Layout
global function new for QuaternionTypes(w: Part, x: Part, y: Part, z: Part): Self {
return Self{wPart := w, xPart := x, yPart := y, zPart := z}
}global function identity for QuaternionTypes(): Self {
return Self.new(1, 0, 0, 0)
}global function fromAxisAngle for QuaternionTypes(axis: Vector<Part, 3>, angle: Part): Self {
let unit := axis.normalize()
let half := angle / 2
let s := half.sin()
return Self.new(half.cos(), unit.x() * s, unit.y() * s, unit.z() * s)
}/// rotations about x (roll), then y (pitch), then z (yaw)
global function fromEuler for QuaternionTypes(roll: Part, pitch: Part, yaw: Part): Self {
let (cr, sr) := ((roll / 2).cos(), (roll / 2).sin())
let (cp, sp) := ((pitch / 2).cos(), (pitch / 2).sin())
let (cy, sy) := ((yaw / 2).cos(), (yaw / 2).sin())
return Self.new(
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
)
}global method w for QuaternionTypes(self: Self): Part {
return self.wPart
}global method x for QuaternionTypes(self: Self): Part {
return self.xPart
}global method y for QuaternionTypes(self: Self): Part {
return self.yPart
}global method z for QuaternionTypes(self: Self): Part {
return self.zPart
}global method scalar for QuaternionTypes(self: Self): Part {
return self.w()
}global method vector for QuaternionTypes(self: Self): Vector<Part, 3> {
return Vector<Part, 3>.fromArray([self.x(), self.y(), self.z()]) !? Vector<Part, 3>.zero()
}global method magnitude for QuaternionTypes(self: Self): Part {
return self.dot(self).sqrt()
}global method normalize for QuaternionTypes(self: Self): Self {
return self.scale(1 / self.magnitude())
}global method conjugate for QuaternionTypes(self: Self): Self {
return Self.new(self.w(), -self.x(), -self.y(), -self.z())
}global method inverse for QuaternionTypes(self: Self): Self {
return self.conjugate().scale(1 / self.dot(self))
}global method dot for QuaternionTypes(self: Self, other: Self): Part {
return self.w() * other.w() + self.x() * other.x() + self.y() * other.y() + self.z() * other.z()
}/// q * p * q⁻¹, with the point as a pure quaternion
global method rotate for QuaternionTypes(self: Self, point: Vector<Part, 3>): Vector<Part, 3> {
let p := Self.new(0, point.x(), point.y(), point.z())
return (self * p * self.inverse()).vector()
}/// spherical interpolation along the shorter arc - falls back to a normalized lerp when nearly parallel
global method slerp for QuaternionTypes(self: Self, to: Self, t: Part): Self {
let target := to
let cosine := self.dot(to)
if cosine < 0 then {
set target := -to
set cosine := -cosine
}
if cosine > 1 - Part.epsilon() * 1000 then { return (self + (target - self).scale(t)).normalize() }
let angle := cosine.acos()
let sine := angle.sin()
return self.scale(((1 - t) * angle).sin() / sine) + target.scale((t * angle).sin() / sine)
}global method toAxisAngle for QuaternionTypes(self: Self): (Vector<Part, 3>, Part) {
let unit := self.normalize()
let angle := 2 * unit.w().min(1).max(-1).acos()
let s := (1 - unit.w() * unit.w()).sqrt()
if s < Part.epsilon() then { return (Vector<Part, 3>.unit(0) !? Vector<Part, 3>.zero(), angle) }
return (unit.vector().scale(1 / s), angle)
}global method toEuler for QuaternionTypes(self: Self): (Part, Part, Part) {
let (w, x, y, z) := (self.w(), self.x(), self.y(), self.z())
let roll := (2 * (w * x + y * z)).atan2(1 - 2 * (x * x + y * y))
let pitch := (2 * (w * y - z * x)).min(1).max(-1).asin()
let yaw := (2 * (w * z + x * y)).atan2(1 - 2 * (y * y + z * z))
return (roll, pitch, yaw)
}global method toMatrix for QuaternionTypes(self: Self): Matrix<Part, 3, 3> {
let (w, x, y, z) := self.normalize() |> λ(q) (q.w(), q.x(), q.y(), q.z())
return Matrix<Part, 3, 3>.fromRows([
[1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
[2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
[2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
]) !? Matrix<Part, 3, 3>.identity()
}global method approxEquals for QuaternionTypes(self: Self, other: Self, tolerance: Part := Part.epsilon()): Boolean {
return (self - other).magnitude() <= tolerance
}#equatable-for-quaterniontypes uses #builtin.intrinsic
method equals(self: Self, other: Self): Boolean {
return #builtin.intrinsic.EQ_Self_Self(self, other)
}#hashable-for-quaterniontypesmethod hash(self: Self): U64 {
return [self.w(), self.x(), self.y(), self.z()].reduce(&hashSeed, λ(hash, part) hashCombine(hash, part.hash()))
}#tostring-for-quaterniontypes/// "1+2i-3j+4k"
method toString(self: Self): String {
let term := λ(value, unit) (value < 0 ? "-" : "+") ++ value.abs().toString() ++ unit
return self.w().toString() ++ term(self.x(), "i") ++ term(self.y(), "j") ++ term(self.z(), "k")
}#fromstring-for-quaterniontypes/// "w+xi+yj+zk" - each part is optional, in any order, like a sum of terms
function parse(text: String): ParseError ! Self {
let parts := [0 as Part, 0 as Part, 0 as Part, 0 as Part]
let terms := Regex("([+-]?[^+-]*[0-9.][^+-]*|[+-]?[ijk])")!!
for term in terms.findAll(text.replaceAll(" ", "")) {
let t := term.text()
let unit := "ijk".indexOf(t.slice((t.size() - 1)..<t.size()) !? "")
let digits := unit == undefined ? t : t.slice(0..<(t.size() - 1)) !? ""
let value := digits == "" or digits == "+" ? 1 : digits == "-" ? -1 : Part.parse(digits)!!
set parts[unit == undefined ? 0 : unit + 1] += value
}
return Self.new(parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0, parts[3] ?? 0)
}#numeric-for-quaterniontypesfunction zero(): Self {
return Self.new(0, 0, 0, 0)
}function one(): Self {
return Self.identity()
} uses #builtin.intrinsic
method add(self: Self, other: Self): Self {
return #builtin.intrinsic.ADD_Self_Self(self, other)
} uses #builtin.intrinsic
method subtract(self: Self, other: Self): Self {
return #builtin.intrinsic.SUB_Self_Self(self, other)
} uses #builtin.intrinsic
/// the Hamilton product - not commutative
method multiply(self: Self, other: Self): Self {
return #builtin.intrinsic.MUL_Self_Self(self, other)
} uses #builtin.intrinsic
/// self * other⁻¹
method divide(self: Self, other: Self): Self {
return #builtin.intrinsic.DIV_Self_Self(self, other)
}method floorDivide(self: Self, other: Self): Self {
let q := self / other
return Self.new(q.w().floor(), q.x().floor(), q.y().floor(), q.z().floor())
}method remainder(self: Self, other: Self): Self {
return self - other * self.floorDivide(other)
} uses #builtin.intrinsic
method power(self: Self, exponent: Self): Self {
return #builtin.intrinsic.POW_Self_Self(self, exponent)
} uses #builtin.intrinsic
method negate(self: Self): Self {
return #builtin.intrinsic.NEG_Self(self)
}method plus(self: Self): Self {
return self
}#scalable-part-for-quaterniontypesmethod scale(self: Self, factor: Part): Self {
return Self.new(self.w() * factor, self.x() * factor, self.y() * factor, self.z() * factor)
}#castable-target-for-quaterniontypesone copy per pair of quaternion types; casting to Self itself is self
uses #builtin.intrinsic
method cast(self: Self): Target {
if Target == Self then { return self }
return #builtin.intrinsic.CAST_Self_Target(self)
}#default-for-quaterniontypesfunction default(): Self {
return Self.new(0, 0, 0, 0)
}#copy-for-quaterniontypesNothing to write - a marker interface, or one another block already covers.
#decimala whole coefficient and how many of its digits are after the point - 12.50 is units 1250, scale 2
Layout
/// how many digits / keeps after the point at least
local const divisionPlaces: U8 := 16local function pow10 for Decimal(count: U64): S128 {
let result: S128 := 1
for _ in 0..<count {
set result := result * 10
}
return result
}/// numerator / denominator as a whole number, the dropped part rounded by mode
local function divideRounded for Decimal(numerator: S128, denominator: S128, mode: RoundingMode): S128 {
let n := denominator < 0 ? -numerator : numerator
let d := denominator < 0 ? -denominator : denominator
let quotient := n / d
let rest := n % d
if rest == 0 then {
return quotient
}
let away: S128 := n < 0 ? -1 : 1
let twice := rest.abs() * 2
match mode {
case RoundingMode.Down then { return quotient }
case RoundingMode.Up then { return quotient + away }
case RoundingMode.Floor then { return away < 0 ? quotient - 1 : quotient }
case RoundingMode.Ceiling then { return away > 0 ? quotient + 1 : quotient }
case RoundingMode.HalfUp then { return twice >= d ? quotient + away : quotient }
case RoundingMode.HalfDown then { return twice > d ? quotient + away : quotient }
case RoundingMode.HalfEven then { return twice > d or (twice == d and quotient % 2 != 0) ? quotient + away : quotient }
}
}/// the units at more places - the same number
local method widen for Decimal(self: Decimal, places: U8): S128 {
return self.units * Self.pow10((places - self.scale) as U64)
} uses #builtin.forever
/// trailing zeros dropped, down to keep places
local method trim for Decimal(self: Decimal, keep: U8): Decimal {
let units := self.units
let scale := self.scale
for _ in #builtin.forever() while scale > keep and units % 10 == 0 {
set units := units / 10
set scale := scale - 1
}
return Decimal{units, scale}
}global function of for Decimal(value: S64): Decimal {
return Decimal{units := value as S128, scale := 0}
}global function fromParts for Decimal(coefficient: S128, places: U8): Decimal {
return Decimal{units := coefficient, scale := places}
}global function fromF64 for Decimal(value: F64, places: U8): ParseError ! Decimal {
if not value.isFinite() then { error ParseError.new(value.toString() ++ " has no exact decimal value") }
return Decimal.parse(value.toString())!!.round(places)
}global method coefficient for Decimal(self: Decimal): S128 {
return self.units
}global method places for Decimal(self: Decimal): U8 {
return self.scale
}global method round for Decimal(self: Decimal, places: U8, mode: RoundingMode := RoundingMode.HalfEven): Decimal {
if places >= self.scale then {
return Decimal{units := self.widen(places), scale := places}
}
return Decimal{units := Self.divideRounded(self.units, Self.pow10((self.scale - places) as U64), mode), scale := places}
} uses #runtime.abort
/// self.units / 10^self.scale / (other.units / 10^other.scale) at places digits is
/// self.units * 10^(places - self.scale + other.scale) / other.units
global method divideTo for Decimal(self: Decimal, other: Decimal, places: U8, mode: RoundingMode := RoundingMode.HalfEven): Decimal {
if other.units == 0 then { #runtime.abort(Error.new("Decimal division by zero")) }
let shift := (places as S64) - (self.scale as S64) + (other.scale as S64)
let numerator := shift >= 0 ? self.units * Self.pow10(shift as U64) : self.units
let denominator := shift >= 0 ? other.units : other.units * Self.pow10((-shift) as U64)
return Decimal{units := Self.divideRounded(numerator, denominator, mode), scale := places}
}global method normalize for Decimal(self: Decimal): Decimal {
return self.trim(0)
}global method abs for Decimal(self: Decimal): Decimal {
return Decimal{units := self.units.abs(), scale := self.scale}
}global method sign for Decimal(self: Decimal): S8 {
return self.units < 0 ? -1 : self.units > 0 ? 1 : 0
}global method isZero for Decimal(self: Decimal): Boolean {
return self.units == 0
}global method isNegative for Decimal(self: Decimal): Boolean {
return self.units < 0
}global method isWhole for Decimal(self: Decimal): Boolean {
return self.units % Self.pow10(self.scale as U64) == 0
}global method toF64 for Decimal(self: Decimal): F64 {
return F64.parse(self.toString()) !? 0.0
}global method toS64 for Decimal(self: Decimal): OverflowError ! S64 {
return (self.units / Self.pow10(self.scale as U64)) as S64
}#numeric-for-decimalfunction zero(): Decimal {
return Decimal{units := 0, scale := 0}
}function one(): Decimal {
return Decimal{units := 1, scale := 0}
}/// the larger side's places
method add(self: Decimal, other: Decimal): Decimal {
let places := self.scale.max(other.scale)
return Decimal{units := self.widen(places) + other.widen(places), scale := places}
}method subtract(self: Decimal, other: Decimal): Decimal {
let places := self.scale.max(other.scale)
return Decimal{units := self.widen(places) - other.widen(places), scale := places}
}/// the places add up - 1.5 * 2.25 is 3.375
method multiply(self: Decimal, other: Decimal): Decimal {
return Decimal{units := self.units * other.units, scale := self.scale + other.scale}
}/// at least 16 places, rounded half to even - zeros past either side's places dropped
method divide(self: Decimal, other: Decimal): Decimal {
let keep := self.scale.max(other.scale)
return self.divideTo(other, keep.max(divisionPlaces)).trim(keep)
}method floorDivide(self: Decimal, other: Decimal): Decimal {
return self.divideTo(other, 0, RoundingMode.Floor)
}/// what's left past the whole times other fits - its sign is self's
method remainder(self: Decimal, other: Decimal): Decimal {
let places := self.scale.max(other.scale)
return Decimal{units := self.widen(places) % other.widen(places), scale := places}
} uses #runtime.abort
/// a whole exponent only - a negative one divides
method power(self: Decimal, exponent: Decimal): Decimal {
if not exponent.isWhole() then { #runtime.abort(Error.new("Decimal powers take a whole exponent, got " ++ exponent.toString())) }
let count := exponent.toS64() !? 0
let result := Decimal.one()
for _ in 0..<(count.abs() as U64) {
set result := result * self
}
return count < 0 ? Decimal.one() / result : result
}method negate(self: Decimal): Decimal {
return Decimal{units := -self.units, scale := self.scale}
}method plus(self: Decimal): Decimal {
return self
}#comparable-for-decimalmethod compare(self: Decimal, other: Decimal): Ordering {
let places := self.scale.max(other.scale)
return self.widen(places).compare(other.widen(places))
}#equatable-for-decimalthe same number - 12.50 equals 12.5
method equals(self: Decimal, other: Decimal): Boolean {
return self.compare(other) == Ordering.Equal
}#hashable-for-decimalmethod hash(self: Decimal): U64 {
let normal := self.normalize()
return normal.units.hash() ^ normal.scale.hash()
}#tostring-for-decimalits places kept - "12.50"
method toString(self: Decimal): String {
let digits := self.units.abs().toString().padStart((self.scale as U64) + 1, "0")
let split := digits.size() - (self.scale as U64)
let whole := digits.slice(0..<split) !? digits
let sign := self.units < 0 ? "-" : ""
return self.scale == 0 ? sign ++ whole : sign ++ whole ++ "." ++ (digits.slice(split..<digits.size()) !? "")
}#fromstring-for-decimal"12.50", "-3", "1_000.5", "1.5e3" - the places written are kept
function parse(text: String): ParseError ! Decimal {
let found := #regex"^([+-]?)([0-9][0-9_]*)?(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?quot;.match(text.trim())
if found == undefined or (found.group(2) == undefined and found.group(3) == undefined) then {
error ParseError.new("not a decimal number: " ++ text)
}
let fraction := found.group(3) ?? ""
let digits := (found.group(2) ?? "0").replaceAll("_", "") ++ fraction
let places := (fraction.size() as S64) - (S64.parse(found.group(4) ?? "0") !? 0)
let units := S128.parse(digits)!! * (places < 0 ? Self.pow10((-places) as U64) : 1)
return Decimal{units := found.group(1) == "-" ? -units : units, scale := places.max(0) as U8}
}#default-for-decimalfunction default(): Decimal {
return Decimal.zero()
}#copy-for-decimalNothing to write - a marker interface, or one another block already covers.
#roundingmodeNo members of its own.
#equatable-for-roundingmodemethod equals(self: RoundingMode, other: RoundingMode): Boolean {
return self.index() == other.index()
}#hashable-for-roundingmodemethod hash(self: RoundingMode): U64 {
return self.index().hash()
}#tostring-for-roundingmodeits name alone
method toString(self: RoundingMode): String {
return ["HalfEven", "HalfUp", "HalfDown", "Up", "Down", "Ceiling", "Floor"][self.index()]!!
}#copy-for-roundingmodeNothing to write - a marker interface, or one another block already covers.
#bigintegera sign and the size in base 10^9 limbs, least significant first, no zero limbs on top - zero has none
Layout
local const limbBase: S64 := 1_000_000_000/// limbs without the zero limbs on top, zero never negative
local function make for BigInteger(negative: Boolean, limbs: Array<S64>): BigInteger {
for _ in Iterator.forever() while limbs.last() == 0 {
limbs.pop()
}
return BigInteger{negative := negative and not limbs.isEmpty(), limbs}
}/// -1, 0 or 1 - the sizes alone
local function compareSizes for BigInteger(a: Array<S64>, b: Array<S64>): S64 {
if a.size() != b.size() then {
return a.size() < b.size() ? -1 : 1
}
for i in (0..<a.size()).reverse() {
if a[i]!! != b[i]!! then {
return a[i]!! < b[i]!! ? -1 : 1
}
}
return 0
}local function addSizes for BigInteger(a: Array<S64>, b: Array<S64>): Array<S64> {
let out: Array<S64> := []
let carry: S64 := 0
for i in 0..<a.size().max(b.size()) {
let total := (a[i] ?? 0) + (b[i] ?? 0) + carry
out.push(total % limbBase)
set carry := total / limbBase
}
if carry > 0 then { out.push(carry) }
return out
}/// a - b, a at least b
local function subtractSizes for BigInteger(a: Array<S64>, b: Array<S64>): Array<S64> {
let out: Array<S64> := []
let borrow: S64 := 0
for i in 0..<a.size() {
let difference := a[i]!! - (b[i] ?? 0) - borrow
set borrow := difference < 0 ? 1 : 0
out.push(difference + borrow * limbBase)
}
return out
}/// schoolbook - a limb times a limb plus what's there fits an S64
local function multiplySizes for BigInteger(a: Array<S64>, b: Array<S64>): Array<S64> {
let out := Array.filled(a.size() + b.size(), 0 as S64)
for i in 0..<a.size() {
let carry: S64 := 0
for j in 0..<b.size() {
let total := out[i + j]!! + a[i]!! * b[j]!! + carry
set out[i + j] := total % limbBase
set carry := total / limbBase
}
set out[i + b.size()] := carry
}
return out
}/// the quotient and what's left of a / b, b not zero - Knuth's long division (TAOCP 4.3.1, algorithm D):
/// both scaled so b's top limb is at least half the base, then each quotient limb guessed from the top
/// limbs, at most 2 too large, and corrected
local function divideSizes for BigInteger(a: Array<S64>, b: Array<S64>): (Array<S64>, Array<S64>) {
if Self.compareSizes(a, b) < 0 then {
return ([], a)
}
let n := b.size()
let m := a.size() - n
let factor := limbBase / (b[n - 1]!! + 1)
let u := Self.multiplySizes(a, [factor])
u.resize(a.size() + 1, 0)
let v := Self.multiplySizes(b, [factor])
let quotient := Array.filled(m + 1, 0 as S64)
for j in (0..=m).reverse() {
let top := u[j + n]!! * limbBase + u[j + n - 1]!!
let guess := top / v[n - 1]!!
let rest := top % v[n - 1]!!
for _ in Iterator.forever() while rest < limbBase and (guess >= limbBase or (n > 1 and guess * v[n - 2]!! > rest * limbBase + u[j + n - 2]!!)) {
set guess := guess - 1
set rest := rest + v[n - 1]!!
}
/// u[j ..= j + n] minus guess times v - one v back when it went below zero
let product := Self.multiplySizes(v, [guess])
let window := u.slice(j..<(j + n + 1))!!
if Self.compareSizes(Self.make(false, window.toArray()).limbs, Self.make(false, product.toArray()).limbs) < 0 then {
set guess := guess - 1
set product := Self.subtractSizes(product, v)
}
let left := Self.subtractSizes(window, product)
for i in 0..=n {
set u[j + i] := left[i] ?? 0
}
set quotient[j] := guess
}
let (rest, _) := Self.divideSizes(Self.make(false, u.slice(0..<n)!!).limbs, [factor])
return (Self.make(false, quotient).limbs, rest)
} uses #runtime.abort
/// self / other's sizes - other zero panics
local function division for BigInteger(self: BigInteger, other: BigInteger): (Array<S64>, Array<S64>) {
if other.isZero() then { #runtime.abort(Error.new("BigInteger division by zero")) }
return Self.divideSizes(self.limbs, other.limbs)
}global function of for BigInteger(value: S64): BigInteger {
let size := (value as S128).abs()
let limbs: Array<S64> := []
for _ in Iterator.forever() while size > 0 {
limbs.push((size % limbBase) as S64)
set size := size / limbBase
}
return BigInteger{negative := value < 0, limbs}
}global method abs for BigInteger(self: BigInteger): BigInteger {
return BigInteger{negative := false, limbs := self.limbs}
}global method sign for BigInteger(self: BigInteger): S8 {
return self.negative ? -1 : self.limbs.isEmpty() ? 0 : 1
}global method isZero for BigInteger(self: BigInteger): Boolean {
return self.limbs.isEmpty()
}global method isPositive for BigInteger(self: BigInteger): Boolean {
return not self.negative and not self.limbs.isEmpty()
}global method isNegative for BigInteger(self: BigInteger): Boolean {
return self.negative
}global method isEven for BigInteger(self: BigInteger): Boolean {
return (self.limbs.first() ?? 0).isEven()
}global method isOdd for BigInteger(self: BigInteger): Boolean {
return not self.isEven()
}global method min for BigInteger(self: BigInteger, other: BigInteger): BigInteger {
return other < self ? other : self
}global method max for BigInteger(self: BigInteger, other: BigInteger): BigInteger {
return other > self ? other : self
}/// by squaring
global method pow for BigInteger(self: BigInteger, exponent: U32): BigInteger {
let result := BigInteger.one()
let base := self
let remaining := exponent
for _ in Iterator.forever() while remaining > 0 {
if remaining.isOdd() then { set result := result * base }
set base := base * base
set remaining := remaining shr 1
}
return result
}/// Newton's steps down from a power of ten above the root
global method sqrt for BigInteger(self: BigInteger): CastError ! BigInteger {
if self.negative then { error CastError.new("no integer square root of " ++ self.toString()) }
if self.isZero() then { return self }
let x := BigInteger.of(10).pow(((self.digitCount() + 1) / 2) as U32)
for _ in Iterator.forever() {
let next := (x + self / x) / BigInteger.of(2)
if next >= x then { return x }
set x := next
}
}global method gcd for BigInteger(self: BigInteger, other: BigInteger): BigInteger {
let a := self.abs()
let b := other.abs()
for _ in Iterator.forever() while not b.isZero() {
set (a, b) := (b, a % b)
}
return a
}global method lcm for BigInteger(self: BigInteger, other: BigInteger): BigInteger {
return self.isZero() or other.isZero() ? BigInteger.zero() : self.abs() / self.gcd(other) * other.abs()
}global method divMod for BigInteger(self: BigInteger, other: BigInteger): (BigInteger, BigInteger) {
return (self // other, self % other)
}global method modulo for BigInteger(self: BigInteger, other: BigInteger): BigInteger {
let rest := self % other
return not rest.isZero() and rest.negative != other.negative ? rest + other : rest
}global method digitCount for BigInteger(self: BigInteger): U64 {
return self.isZero() ? 1 : (self.limbs.size() - 1) * 9 + self.limbs.last()!!.toString().size()
}global method toF64 for BigInteger(self: BigInteger): F64 {
return F64.parse(self.toString()) !? 0.0
}/// through the widest integer of the sign, then `as`
global method toInteger for BigInteger<Target>(self: BigInteger): OverflowError ! Target {
let wide := self.negative ? (S256.parse(self.toString()) !? undefined) as Unknown : (U256.parse(self.toString()) !? undefined) as Unknown
if wide == undefined then { error OverflowError.new(self.toString() ++ " doesn't fit in a " ++ Target.name()) }
return (wide.checkedCast<Target>() !? undefined) ?? error OverflowError.new(self.toString() ++ " doesn't fit in a " ++ Target.name())
}#numeric-for-bigintegerfunction zero(): BigInteger {
return BigInteger{negative := false, limbs := []}
}function one(): BigInteger {
return BigInteger{negative := false, limbs := [1]}
}method add(self: BigInteger, other: BigInteger): BigInteger {
if self.negative == other.negative then {
return Self.make(self.negative, Self.addSizes(self.limbs, other.limbs))
}
if Self.compareSizes(self.limbs, other.limbs) >= 0 then {
return Self.make(self.negative, Self.subtractSizes(self.limbs, other.limbs))
}
return Self.make(other.negative, Self.subtractSizes(other.limbs, self.limbs))
}method subtract(self: BigInteger, other: BigInteger): BigInteger {
return self + -other
}method multiply(self: BigInteger, other: BigInteger): BigInteger {
return Self.make(self.negative != other.negative, Self.multiplySizes(self.limbs, other.limbs))
}/// toward zero, like the fixed integers
method divide(self: BigInteger, other: BigInteger): BigInteger {
let (quotient, _) := self.division(other)
return Self.make(self.negative != other.negative, quotient)
}method floorDivide(self: BigInteger, other: BigInteger): BigInteger {
let (quotient, rest) := self.division(other)
let truncated := Self.make(self.negative != other.negative, quotient)
return self.negative != other.negative and not rest.isEmpty() ? truncated - BigInteger.one() : truncated
}/// its sign is self's
method remainder(self: BigInteger, other: BigInteger): BigInteger {
let (_, rest) := self.division(other)
return Self.make(self.negative, rest)
} uses #runtime.abort
/// an exponent of 0 or more
method power(self: BigInteger, exponent: BigInteger): BigInteger {
if exponent.negative then { #runtime.abort(Error.new("BigInteger powers take an exponent of 0 or more, got " ++ exponent.toString())) }
return self.pow(exponent.toInteger<U32>()!!)
}method negate(self: BigInteger): BigInteger {
return BigInteger{negative := not self.negative and not self.limbs.isEmpty(), limbs := self.limbs}
}method plus(self: BigInteger): BigInteger {
return self
}#comparable-for-bigintegermethod compare(self: BigInteger, other: BigInteger): Ordering {
if self.negative != other.negative then {
return self.negative ? Ordering.Less : Ordering.Greater
}
let sizes := Self.compareSizes(self.limbs, other.limbs)
return (self.negative ? -sizes : sizes).compare(0)
}#equatable-for-bigintegermethod equals(self: BigInteger, other: BigInteger): Boolean {
return self.negative == other.negative and self.limbs == other.limbs
}#hashable-for-bigintegermethod hash(self: BigInteger): U64 {
return self.negative.hash() ^ self.limbs.hash()
}#tostring-for-bigintegerthe top limb as it is, every other one 9 digits
method toString(self: BigInteger): String {
if self.isZero() then { return "0" }
let digits := self.limbs.reverse().mapIndexed(λ(i, limb) → i == 0 ? limb.toString() : limb.toString().padStart(9, "0"))
return (self.negative ? "-" : "") ++ digits.join("")
}#fromstring-for-bigintegerdecimal digits, optional sign, _ separators
function parse(text: String): ParseError ! BigInteger {
let found := #regex"^([+-]?)([0-9][0-9_]*)quot;.match(text.trim())
if found == undefined then { error ParseError.new("not a whole number: " ++ text) }
let digits := found.group(2)!!.replaceAll("_", "")
let limbs: Array<S64> := []
let end := digits.size()
for _ in Iterator.forever() while end > 0 {
let start := end > 9 ? end - 9 : 0
limbs.push(S64.parse(digits.slice(start..<end)!!)!!)
set end := start
}
return Self.make(found.group(1) == "-", limbs)
}#default-for-bigintegerfunction default(): BigInteger {
return BigInteger.zero()
}#rationalLayout
uses #runtime.abort
/// top / bottom in lowest terms, the bottom positive - a zero bottom panics
local function make for Rational(top: BigInteger, bottom: BigInteger): Rational {
if bottom.isZero() then { #runtime.abort(Error.new("Rational with a zero denominator")) }
let common := top.gcd(bottom)
let sign := bottom.isNegative() ? BigInteger.of(-1) : BigInteger.one()
return Rational{top := top / common * sign, bottom := bottom / common * sign}
}/// top / bottom (bottom positive) as a whole number, the dropped part rounded by mode - as Decimal rounds
local function rounded for Rational(top: BigInteger, bottom: BigInteger, mode: RoundingMode): BigInteger {
let quotient := top / bottom
let rest := top % bottom
if rest.isZero() then { return quotient }
let away := top.isNegative() ? BigInteger.of(-1) : BigInteger.one()
let half := (rest.abs() * BigInteger.of(2)).compare(bottom)
return match mode {
case RoundingMode.Down then quotient
case RoundingMode.Up then quotient + away
case RoundingMode.Floor then top.isNegative() ? quotient - BigInteger.one() : quotient
case RoundingMode.Ceiling then top.isNegative() ? quotient : quotient + BigInteger.one()
case RoundingMode.HalfUp then half != Ordering.Less ? quotient + away : quotient
case RoundingMode.HalfDown then half == Ordering.Greater ? quotient + away : quotient
case RoundingMode.HalfEven then half == Ordering.Greater or (half == Ordering.Equal and quotient.isOdd()) ? quotient + away : quotient
}
}global function of for Rational(numerator: S64, denominator: S64 := 1): Rational {
return Self.make(BigInteger.of(numerator), BigInteger.of(denominator))
}global function fromParts for Rational(numerator: BigInteger, denominator: BigInteger): Rational {
return Self.make(numerator, denominator)
}global function fromDecimal for Rational(value: Decimal): Rational {
return Self.make(value.coefficient().toBigInteger(), BigInteger.of(10).pow(value.places() as U32))
}global method numerator for Rational(self: Rational): BigInteger {
return self.top
}global method denominator for Rational(self: Rational): BigInteger {
return self.bottom
}global method reciprocal for Rational(self: Rational): Rational {
return Self.make(self.bottom, self.top)
}global method abs for Rational(self: Rational): Rational {
return Rational{top := self.top.abs(), bottom := self.bottom}
}global method sign for Rational(self: Rational): S8 {
return self.top.sign()
}global method isZero for Rational(self: Rational): Boolean {
return self.top.isZero()
}global method isNegative for Rational(self: Rational): Boolean {
return self.top.isNegative()
}global method isWhole for Rational(self: Rational): Boolean {
return self.bottom == BigInteger.one()
}global method floor for Rational(self: Rational): BigInteger {
return Self.rounded(self.top, self.bottom, RoundingMode.Floor)
}global method ceil for Rational(self: Rational): BigInteger {
return Self.rounded(self.top, self.bottom, RoundingMode.Ceiling)
}global method truncate for Rational(self: Rational): BigInteger {
return Self.rounded(self.top, self.bottom, RoundingMode.Down)
}global method round for Rational(self: Rational, mode: RoundingMode := RoundingMode.HalfEven): BigInteger {
return Self.rounded(self.top, self.bottom, mode)
}global method toDecimal for Rational(self: Rational, places: U8, mode: RoundingMode := RoundingMode.HalfEven): OverflowError ! Decimal {
let coefficient := Self.rounded(self.top * BigInteger.of(10).pow(places as U32), self.bottom, mode)
return Decimal.fromParts(coefficient.toInteger<S128>()!!, places)
}/// 20 significant digits worked out exactly, then read as an F64
global method toF64 for Rational(self: Rational): F64 {
if self.isZero() then { return 0.0 }
let shift := (self.bottom.digitCount() as S64) - (self.top.digitCount() as S64) + 20
let scaled := shift > 0 ? self.top * BigInteger.of(10).pow(shift as U32) / self.bottom : self.top / (self.bottom * BigInteger.of(10).pow((-shift) as U32))
return F64.parse(scaled.toString() ++ "e" ++ (-shift).toString()) !? 0.0
}#numeric-for-rationalfunction zero(): Rational {
return Rational{top := BigInteger.zero(), bottom := BigInteger.one()}
}function one(): Rational {
return Rational{top := BigInteger.one(), bottom := BigInteger.one()}
}method add(self: Rational, other: Rational): Rational {
return Self.make(self.top * other.bottom + other.top * self.bottom, self.bottom * other.bottom)
}method subtract(self: Rational, other: Rational): Rational {
return Self.make(self.top * other.bottom - other.top * self.bottom, self.bottom * other.bottom)
}method multiply(self: Rational, other: Rational): Rational {
return Self.make(self.top * other.top, self.bottom * other.bottom)
} uses #runtime.abort
method divide(self: Rational, other: Rational): Rational {
if other.isZero() then { #runtime.abort(Error.new("Rational division by zero")) }
return Self.make(self.top * other.bottom, self.bottom * other.top)
}method floorDivide(self: Rational, other: Rational): Rational {
return Rational{top := (self / other).floor(), bottom := BigInteger.one()}
}/// its sign is self's
method remainder(self: Rational, other: Rational): Rational {
return self - other * Rational{top := (self / other).truncate(), bottom := BigInteger.one()}
} uses #runtime.abort
/// a whole exponent only - a negative one divides
method power(self: Rational, exponent: Rational): Rational {
if not exponent.isWhole() then { #runtime.abort(Error.new("Rational powers take a whole exponent, got " ++ exponent.toString())) }
let count := exponent.top.abs().toInteger<U32>()!!
let raised := Rational{top := self.top.pow(count), bottom := self.bottom.pow(count)}
return exponent.isNegative() ? raised.reciprocal() : raised
}method negate(self: Rational): Rational {
return Rational{top := -self.top, bottom := self.bottom}
}method plus(self: Rational): Rational {
return self
}#comparable-for-rationalmethod compare(self: Rational, other: Rational): Ordering {
return (self.top * other.bottom).compare(other.top * self.bottom)
}#equatable-for-rationallowest terms make one way to write each number
method equals(self: Rational, other: Rational): Boolean {
return self.top == other.top and self.bottom == other.bottom
}#hashable-for-rationalmethod hash(self: Rational): U64 {
return self.top.hash() ^ self.bottom.hash()
}#tostring-for-rational"1/3", or "2" when it's whole
method toString(self: Rational): String {
return self.isWhole() ? self.top.toString() : self.top.toString() ++ "/" ++ self.bottom.toString()
}#fromstring-for-rational"1/3", "-6/4", "2", "0.75"
function parse(text: String): ParseError ! Rational {
let found := #regex"^([+-]?[0-9][0-9_]*)(?:/([+-]?[0-9][0-9_]*)|\.([0-9_]+))?quot;.match(text.trim())
if found == undefined then { error ParseError.new("not a fraction: " ++ text) }
let fraction := (found.group(3) ?? "").replaceAll("_", "")
let top := BigInteger.parse(found.group(1)!! ++ fraction)!!
let bottom := found.group(2) == undefined ? BigInteger.of(10).pow(fraction.size() as U32) : BigInteger.parse(found.group(2)!!)!!
if bottom.isZero() then { error ParseError.new("a zero denominator: " ++ text) }
return Self.make(top, bottom)
}#default-for-rationalfunction default(): Rational {
return Rational.zero()
}#quantity-type-unitthe unit lives only in the type, where the checker keeps different ones apart - a value keeps how it's written, for toString, and what it comes to, for as and convert: its scale and offset from its base units, and those base units each to a power
Layout
/// a unit's name inside another's - "Meter/Second" in a product is "(Meter/Second)"
local function part for Quantity<Type, Unit>(name: String): String {
return name.contains("/") or name.contains("·") ? "(" ++ name ++ ")" : name
}/// "Meter" raised to a power - "Meter²", "Meter³"; the digits are written raised
local function power for Quantity<Type, Unit>(name: String, times: U64): String {
let raised := ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"]
return name ++ times.toString().codepoints().map((digit) => raised[digit.toString().parse<U64>()!!]).join("")
}/// a unit split into what it's a power of and the power - "Meter²" is ("Meter", 2), "Meter" is ("Meter", 1)
local function powerOf for Quantity<Type, Unit>(name: String): (String, U64) {
let found := name.match(/^(.+?)([⁰¹²³⁴⁵⁶⁷⁸⁹]+)$/)
if found == undefined then {
return (name, 1)
}
let raised := "⁰¹²³⁴⁵⁶⁷⁸⁹"
let digits := found!!.groups[1].codepoints().map((digit) => raised.indexOf(digit.toString())!!.toString()).join("")
return (found!!.groups[0], digits.parse<U64>()!!)
}/// two units multiplied - "Newton·Meter", or one unit's power: "Meter" times "Meter" is "Meter²"
local function product for Quantity<Type, Unit>(left: String, right: String): String {
let (leftBase, leftPower) := Self.powerOf(left)
let (rightBase, rightPower) := Self.powerOf(right)
if leftBase == rightBase then {
return Self.power(Self.part(leftBase), leftPower + rightPower)
}
return Self.part(left) ++ "·" ++ Self.part(right)
}/// a unit as the compiler names it - its name, then what it comes to: "Kilometer|1000|0|Meter^1" - as a
/// quantity of value in it; a bare name, a unit it can't read (a type parameter), is a base unit of its own
local function inUnit for Quantity<Type, Unit>(value: Type, named: String): Self {
let parts := named.split("|")
if parts.size() < 4 then {
return Self{amount := value, unit := named, scale := 1.0, offset := 0.0, dims := Map.of((named, 1))}
}
let dims: Map<String, S64> := Map.of()
for power in parts[3].split(" ") if power != "" {
let (name, exponent) := power.splitOnce("^")!!
dims.set(name, exponent.toInteger()!!)
}
return Self{amount := value, unit := parts[0], scale := parts[1].toNumber()!!, offset := parts[2].toNumber()!!, dims}
} uses #comptime.typeName
global function of for Quantity<Type, Unit>(value: Type): Quantity<Type, Unit> {
return Self.inUnit(value, #comptime.typeName(Unit))
}/// self in another unit of what it measures - through the base units, scale and offset both - to 12
/// significant digits, past which is the scales' rounding
local function into for Quantity<Type, Unit>(self: Self, unit: String, scale: F64, offset: F64): Self {
let measured := self.amount as F64 * self.scale + self.offset
let amount := ((measured - offset) / scale).roundSignificant(12)
return Self{amount := amount as Type, unit, scale, offset, dims := self.dims}
}global method value for Quantity<Type, Unit>(self: Self): Type {
return self.amount
}global method unitName for Quantity<Type, Unit>(self: Self): String {
return self.unit
}global method add for Quantity<Type, Unit>(self: Self, other: Self): Self {
return Self{amount := self.amount + other.amount, unit := self.unit, scale := self.scale, offset := self.offset, dims := self.dims}
}global method subtract for Quantity<Type, Unit>(self: Self, other: Self): Self {
return Self{amount := self.amount - other.amount, unit := self.unit, scale := self.scale, offset := self.offset, dims := self.dims}
}global method negate for Quantity<Type, Unit>(self: Self): Self {
return Self{amount := -self.amount, unit := self.unit, scale := self.scale, offset := self.offset, dims := self.dims}
}global method ratio for Quantity<Type, Unit>(self: Self, other: Self): Type {
return self.amount / other.amount
}/// base units multiplied (sign 1) or divided (sign -1) - those whose powers cancel left out
local function joined for Quantity<Type, Unit>(left: Map<String, S64>, right: Map<String, S64>, sign: S64): Map<String, S64> {
let out := left.clone()
for (name, power) in right {
out.set(name, (out.get(name) ?? 0) + sign * power)
}
out.retain((name, power) => power != 0)
return out
}/// a product or quotient - a plain number when the units cancel (2 Kilometer / 500 Meter is 4), scaled
/// by what's left of the units' scales
local function made for Quantity<Type, Unit>(amount: Type, unit: String, scale: F64, dims: Map<String, S64>): Quantity<Type, Unknown> | Type {
if dims.isEmpty() then {
return scale == 1.0 ? amount : (amount as F64 * scale).roundSignificant(12) as Type
}
return Quantity{amount, unit, scale, offset := 0.0, dims}
}/// also what a * b between two quantities is
global method times for Quantity<Type, Unit, Other>(self: Self, other: Quantity<Type, Other>): Quantity<Type, Product<Unit, Other>> {
return Self.made(self.amount * other.amount, Self.product(self.unit, other.unit), self.scale * other.scale, Self.joined(self.dims, other.dims, 1))
}/// also what a / b between quantities of two units is
global method per for Quantity<Type, Unit, Other>(self: Self, other: Quantity<Type, Other>): Quantity<Type, Per<Unit, Other>> {
return Self.made(self.amount / other.amount, Self.part(self.unit) ++ "/" ++ Self.part(other.unit), self.scale / other.scale, Self.joined(self.dims, other.dims, -1))
} uses #comptime.typeName
global method convert for Quantity<Type, Unit, Target>(self: Self, factor: Type? := undefined): Quantity<Type, Target> {
let target := Self.inUnit(self.amount, #comptime.typeName(Target))
if factor == undefined then {
return Self.into(self, target.unit, target.scale, target.offset)
}
return Quantity{amount := self.amount * factor, unit := target.unit, scale := target.scale, offset := target.offset, dims := target.dims}
}#castable-quantity-type-target-for-quantity-type-unitquantity as Quantity
method cast(self: Self): Quantity<Type, Target> {
return self.convert<Target>()
}#scalable-type-for-quantity-type-unita * 2.0, 2.0 * a and a / 2.0
method scale(self: Self, factor: Type): Self {
return Self{amount := self.amount * factor, unit := self.unit, scale := self.scale, offset := self.offset, dims := self.dims}
}#equatable-for-quantity-type-unitmethod equals(self: Self, other: Self): Boolean {
return self.amount == other.amount
}#hashable-for-quantity-type-unitmethod hash(self: Self): U64 {
return self.amount.hash()
}#comparable-for-quantity-type-unitmethod compare(self: Self, other: Self): Ordering {
return self.amount.compare(other.amount)
}#tostring-for-quantity-type-unit"3.5 Meter"
method toString(self: Self): String {
return self.amount.toString() ++ " " ++ self.unit
}#copy-for-quantity-type-unitNothing to write - a marker interface, or one another block already covers.
#matha namespace of number functions, called on its name - Math.sqrt(2.0) - with no values of its own
global function pi for Math(): F64 {
return F64.pi()
}global function tau for Math(): F64 {
return F64.tau()
}global function e for Math(): F64 {
return F64.e()
}global function infinity for Math(): F64 {
return F64.infinity()
}global function nan for Math(): F64 {
return F64.nan()
}global function min for Math<T: Comparable>(values: ..T): T {
return values.min() !? abort(EmptyError.new("Math.min of no values"))
}global function max for Math<T: Comparable>(values: ..T): T {
return values.max() !? abort(EmptyError.new("Math.max of no values"))
}global function abs for Math<T: Numeric>(x: T): T {
return x < T.zero() ? -x : x
}global function sign for Math<T: Numeric>(x: T): T {
return x < T.zero() ? -T.one() : x > T.zero() ? T.one() : T.zero()
}global function clamp for Math<T: Comparable>(x: T, low: T, high: T): T {
return x < low ? low : x > high ? high : x
}global function sum for Math<T: Numeric>(values: ..T): T {
return values.sum()
}global function product for Math<T: Numeric>(values: ..T): T {
return values.product()
}global function mean for Math(values: ..F64): EmptyError ! F64 {
if values.isEmpty() then {
error EmptyError.new("Math.mean of no values")
}
return values.sum() ÷ (values.size() as F64)
}global function median for Math(values: ..F64): EmptyError ! F64 {
if values.isEmpty() then {
error EmptyError.new("Math.median of no values")
}
let sorted ≔ values.sorted()
let middle ≔ sorted.size() ÷ 2
let upper ≔ sorted[middle] ?? 0.0
return sorted.size() % 2 = 1 ? upper : (sorted[middle - 1] ?? upper + upper) ÷ 2.0
}global function floor for Math(x: F64): F64 {
return x.floor()
}global function ceil for Math(x: F64): F64 {
return x.ceil()
}/// halves away from zero - to `decimals` places after the point
global function round for Math(x: F64, decimals: U8 ≔ 0): F64 {
return decimals = 0 ? x.round() : x.roundTo(decimals)
}global function trunc for Math(x: F64): F64 {
return x.truncate()
}global function fract for Math(x: F64): F64 {
return x.fraction()
}global function sqrt for Math(x: F64): F64 {
return x.sqrt()
}global function cbrt for Math(x: F64): F64 {
return x.cbrt()
}/// the length of a vector with these parts - each pair taken with hypot, so nothing overflows on the way
global function hypot for Math(values: ..F64): F64 {
let length ≔ 0.0
for value in values {
set length ≔ length.hypot(value)
}
return length
}global function pow for Math(base: F64, exponent: F64): F64 {
return base ^ exponent
}global function exp for Math(x: F64): F64 {
return x.exp()
}global function ln for Math(x: F64): F64 {
return x.ln()
}global function log for Math(x: F64, base: F64): F64 {
return x.log(base)
}global function log2 for Math(x: F64): F64 {
return x.log2()
}global function log10 for Math(x: F64): F64 {
return x.log10()
}global function sin for Math(angle: F64): F64 {
return angle.sin()
}global function cos for Math(angle: F64): F64 {
return angle.cos()
}global function tan for Math(angle: F64): F64 {
return angle.tan()
}global function asin for Math(x: F64): F64 {
return x.asin()
}global function acos for Math(x: F64): F64 {
return x.acos()
}global function atan for Math(x: F64): F64 {
return x.atan()
}/// the angle from the positive x-axis to the point (x, y) - y first, as atan(y / x) has it
global function atan2 for Math(y: F64, x: F64): F64 {
return y.atan2(x)
}global function sinh for Math(x: F64): F64 {
return x.sinh()
}global function cosh for Math(x: F64): F64 {
return x.cosh()
}global function tanh for Math(x: F64): F64 {
return x.tanh()
}global function toRadians for Math(degrees: F64): F64 {
return degrees.toRadians()
}global function toDegrees for Math(radians: F64): F64 {
return radians.toDegrees()
}global function lerp for Math(a: F64, b: F64, t: F64): F64 {
return a + (b - a) * t
}global function inverseLerp for Math(a: F64, b: F64, x: F64): F64 {
return (x - a) ÷ (b - a)
}/// x moved from one range to the same place in another - Math.remap(5.0, 0.0..<10.0, 100.0..<200.0) is 150
global function remap for Math(x: F64, from: Range<F64>, to: Range<F64>): F64 {
let t ≔ Math.inverseLerp(from.start(), from.end(), x)
return Math.lerp(to.start(), to.end(), t)
}global function gcd for Math(a: S64, b: S64): S64 {
return a.gcd(b)
}global function lcm for Math(a: S64, b: S64): S64 {
return a.lcm(b)
}/// 1 * 2 * ... * n - 1 for 0 - an OverflowError past U64
global function factorial for Math(n: U64): OverflowError ! U64 {
let result: U64 ≔ 1
for k in 2..=n {
set result ≔ result.checkedMultiply(k)!!
}
return result
}/// the ways to choose k of n - 0 when k > n; an OverflowError past U64
global function binomial for Math(n: U64, k: U64): OverflowError ! U64 {
if k > n then {
return 0
}
let smaller ≔ k.min(n - k)
let result: U64 ≔ 1
for i in 1..=smaller {
/// exact at each step - result * (n - smaller + i) is a multiple of i
set result ≔ result.checkedMultiply(n - smaller + i)!! ÷ i
}
return result
}global function isPrime for Math(n: U64): Boolean {
if n < 2 then {
return false
}
if n < 4 then {
return true
}
if n % 2 = 0 or n % 3 = 0 then {
return false
}
let d: U64 ≔ 5
for _ in Iterator.forever() while d ≤ n ÷ d {
if n % d = 0 or n % (d + 2) = 0 then {
return false
}
set d ≔ d + 6
}
return true
}global function isNan for Math(x: F64): Boolean {
return x.isNaN()
}global function isFinite for Math(x: F64): Boolean {
return x.isFinite()
}/// a and b at most tolerance apart - relative to the larger, or absolute when both are below 1
global function approxEquals for Math(a: F64, b: F64, tolerance: F64 ≔ 1e-9): Boolean {
let scale ≔ a.abs().max(b.abs()).max(1.0)
return (a - b).abs() ≤ tolerance * scale
}#utf8-utf16-utf32-asciiwritten once for TextTypes; code units are stored little-endian
Layout
/// the code point starting at byte i, and how many bytes it took - undefined when malformed
local function decodeAt for TextTypes(bytes: Bytes, i: U64): (U32, U64)? {
let at := λ(k) (bytes[i + k] ?? 0) as U32
if Self == ASCII then { return at(0) < 0x80 ? (at(0), 1) : undefined }
if Self == UTF32 then { return i + 4 <= bytes.size() ? (bytes.readInteger<U32>(i) !? 0, 4) : undefined }
if Self == UTF16 then {
if i + 2 > bytes.size() then { return undefined }
let high := (bytes.readInteger<U16>(i) !? 0) as U32
if high < 0xD800 or high > 0xDFFF then { return (high, 2) }
if high > 0xDBFF or i + 4 > bytes.size() then { return undefined }
let low := (bytes.readInteger<U16>(i + 2) !? 0) as U32
if low < 0xDC00 or low > 0xDFFF then { return undefined }
return (0x10000 + ((high - 0xD800) shl 10) + (low - 0xDC00), 4)
}
let lead := at(0)
let width: U64 := lead < 0x80 ? 1 : lead >= 0xF8 ? 0 : lead >= 0xF0 ? 4 : lead >= 0xE0 ? 3 : lead >= 0xC0 ? 2 : 0
if width == 0 or i + width > bytes.size() then { return undefined }
let value := width == 1 ? lead : lead band (0x7F shr width)
for k in 1..<width {
if (at(k) band 0xC0) != 0x80 then { return undefined }
set value := (value shl 6) bor (at(k) band 0x3F)
}
/// an overlong form - a value that fits in fewer bytes - isn't valid UTF-8
let smallest: U32 := width == 2 ? 0x80 : width == 3 ? 0x800 : width == 4 ? 0x10000 : 0
return value < smallest ? undefined : (value, width)
}/// every code point in bytes, or a ParseError at the first malformed one (surrogates included -
/// Codepoint.fromU32 turns them down)
local function decodeAll for TextTypes(bytes: Bytes): ParseError ! Array<Codepoint> {
let invalid := ParseError.new("not valid " ++ Self.name())
let result: Array<Codepoint> := []
let i: U64 := 0
for _ in forever() while i < bytes.size() {
let (value, width) := (Self.decodeAt(bytes, i) ?! invalid)!!
result.push(((Codepoint.fromU32(value) !? undefined) ?! invalid)!!)
set i += width
}
return result
}/// one code point in Self's encoding - ASCII has no room for the rest, and writes '?' for them
local function encodeOne for TextTypes(c: Codepoint): Array<U8> {
let v := c.value()
if Self == ASCII then { return [v < 0x80 ? v as U8 : 0x3F] }
if Self == UTF32 then { return v.toBytes().toArray() }
if Self == UTF16 then {
if v < 0x10000 then { return (v as U16).toBytes().toArray() }
let rest := v - 0x10000
return [..((0xD800 + (rest shr 10)) as U16).toBytes(), ..((0xDC00 + (rest band 0x3FF)) as U16).toBytes()]
}
if v < 0x80 then { return [v as U8] }
if v < 0x800 then { return [(0xC0 bor (v shr 6)) as U8, (0x80 bor (v band 0x3F)) as U8] }
if v < 0x10000 then { return [(0xE0 bor (v shr 12)) as U8, (0x80 bor ((v shr 6) band 0x3F)) as U8, (0x80 bor (v band 0x3F)) as U8] }
return [(0xF0 bor (v shr 18)) as U8, (0x80 bor ((v shr 12) band 0x3F)) as U8, (0x80 bor ((v shr 6) band 0x3F)) as U8, (0x80 bor (v band 0x3F)) as U8]
}/// where part next occurs at or after from, in code points, and how many code points it covers
local method findPart for TextTypes(self: Self, part: Self | Regex, from: U64): (U64, U64)? {
match part {
case Regex as regex then {
let found := regex.find(self as String, from)
return found == undefined ? undefined : (found.start(), found.end() - found.start())
}
case _ then {
let text := self.codepoints()
let needle := (part as Self).codepoints()
for start in from..<(text.size() + 1) while start + needle.size() <= text.size() if (0..<needle.size()).all(λ(k) text[start + k] == needle[k]) {
{ return (start, needle.size()) }
}
return undefined
}
}
}global function fromCodepoints for TextTypes(codepoints: Array<Codepoint>): Self {
let bytes: Array<U8> := []
for c in codepoints {
set bytes ++= Self.encodeOne(c)
}
return Self{units := Bytes.fromArray(bytes)}
}global function decode for TextTypes(bytes: Bytes): ParseError ! Self {
Self.decodeAll(bytes)!!
return Self{units := bytes.clone()}
}global function repeat for TextTypes(text: Self, count: U64): Self {
return text.repeat(count)
}global method byteLength for TextTypes(self: Self): U64 {
return self.units.size()
}/// units are valid by construction, so decoding them can't fail
global method codepoints for TextTypes(self: Self): Array<Codepoint> {
return Self.decodeAll(self.units) !? []
}/// extended grapheme clusters - "e" and a combining accent are one
global method graphemes for TextTypes(self: Self): Array<Self> {
let text := self.codepoints()
let starts := #builtin.graphemeBreaks(text)
return starts.mapIndexed(λ(i, start) Self.fromCodepoints(text.slice(start..<(starts[i + 1] ?? text.size())) !? []))
}/// a final line break doesn't start one more, empty line
global method lines for TextTypes(self: Self): Array<Self> {
let result := self.split("\n", undefined).map(λ(line) line.stripSuffix("\r") ?? line)
if result.size() > 1 and (result.last() ?? "").isEmpty() then { result.pop() }
return result
}global method words for TextTypes(self: Self): Array<Self> {
let result: Array<Self> := []
let current: Array<Codepoint> := []
for c in self.codepoints() {
if not c.isWhitespace() then {
current.push(c)
} else {
if not current.isEmpty() then {
result.push(Self.fromCodepoints(current))
set current := []
}
}
}
if not current.isEmpty() then { result.push(Self.fromCodepoints(current)) }
return result
}global method encode for TextTypes(self: Self): Bytes {
return self.units.clone()
}/// the leftmost match starts at 0 whenever any match does
global method startsWith for TextTypes(self: Self, prefix: Self | Regex): Boolean {
return self.findPart(prefix, 0)?[0] == 0
}global method endsWith for TextTypes(self: Self, suffix: Self | Regex): Boolean {
match suffix {
case Regex as regex then {
let anchored := Regex.new("(?:" ++ regex.source() ++ ")quot;, regex.flags()) !? regex
return anchored.test(self as String)
}
case _ then {
let tail := (suffix as Self).size()
return tail <= self.size() and self.findPart(suffix, self.size() - tail)?[0] == self.size() - tail
}
}
}global method indexOf for TextTypes(self: Self, part: Self | Regex, from: U64 := 0): U64? {
return self.findPart(part, from)?[0]
}global method lastIndexOf for TextTypes(self: Self, part: Self | Regex): U64? {
let last: U64? := undefined
let from: U64 := 0
for _ in forever() {
let found := self.findPart(part, from)
if found == undefined then { return last }
set last := found[0]
set from := found[0] + 1
}
}/// non-overlapping, left to right
global method count for TextTypes(self: Self, part: Self | Regex): U64 {
let total: U64 := 0
let from: U64 := 0
for _ in forever() {
let found := self.findPart(part, from)
if found == undefined then { return total }
set total += 1
set from := found[0] + found[1].max(1)
}
}global method slice for TextTypes(self: Self, range: Range<U64>): IndexError ! Self {
return Self.fromCodepoints(self.codepoints().slice(range)!!)
}global method charAt for TextTypes(self: Self, index: U64): Codepoint? {
return self.codepoints()[index]
}/// an empty match splits between code points, but never before the first or after the last
global method split for TextTypes(self: Self, separator: Self | Regex, limit: U64?): Array<Self> {
let text := self.codepoints()
let parts: Array<Self> := []
let from: U64 := 0
for round in forever() {
if limit != undefined and parts.size() + 1 >= limit then { break round }
let found := self.findPart(separator, from)
if found != undefined and found[1] == 0 and found[0] == from then { set found := self.findPart(separator, from + 1) }
if found == undefined or (found[1] == 0 and found[0] >= text.size()) then { break round }
parts.push(Self.fromCodepoints(text.slice(from..<found[0]) !? []))
set from := found[0] + found[1]
}
parts.push(Self.fromCodepoints(text.slice(from..<text.size()) !? []))
return parts
}global method splitOnce for TextTypes(self: Self, separator: Self | Regex): (Self, Self)? {
let found := self.findPart(separator, 0)
if found == undefined then { return undefined }
let text := self.codepoints()
return (Self.fromCodepoints(text.slice(0..<found[0]) !? []), Self.fromCodepoints(text.slice((found[0] + found[1])..<text.size()) !? []))
}global method join for TextTypes(self: Self, parts: Iterable<Self>): Self {
let bytes: Array<U8> := []
for part, i in parts {
if i > 0 then { set bytes ++= self.units }
set bytes ++= part.units
}
return Self{units := Bytes.fromArray(bytes)}
}global method replace for TextTypes(self: Self, part: Self | Regex, with: Self): Self {
match part {
case Regex as regex then { return regex.replace(self as String, with as String) as Self }
case _ then {
let found := self.findPart(part, 0)
if found == undefined then { return self }
let text := self.codepoints()
return Self.fromCodepoints([..(text.slice(0..<found[0]) !? []), ..with.codepoints(), ..(text.slice((found[0] + found[1])..<text.size()) !? [])])
}
}
}global method replaceAll for TextTypes(self: Self, part: Self | Regex, with: Self): Self {
match part {
case Regex as regex then { return regex.replaceAll(self as String, with as String) as Self }
case _ then { return with.join(self.split(part, undefined)) }
}
}global method repeat for TextTypes(self: Self, count: U64): Self {
let bytes: Array<U8> := []
for _ in 0..<count {
set bytes ++= self.units
}
return Self{units := Bytes.fromArray(bytes)}
}global method reverse for TextTypes(self: Self): Self {
return Self.fromCodepoints(self.graphemes().reverse().flatMap(λ(g) g.codepoints()))
}global method insert for TextTypes(self: Self, index: U64, part: Self): IndexError ! Self {
let text := self.codepoints()
if index > text.size() then { error IndexError.new("index " ++ index.toString() ++ " is past the end of the text") }
return Self.fromCodepoints([..(text.slice(0..<index) !? []), ..part.codepoints(), ..(text.slice(index..<text.size()) !? [])])
}global method remove for TextTypes(self: Self, range: Range<U64>): IndexError ! Self {
let text := self.codepoints()
let (start, end) := bounds(range)
if end > text.size() then { error IndexError.new("range " ++ range.toString() ++ " is past the end of the text") }
return Self.fromCodepoints([..(text.slice(0..<start) !? []), ..(text.slice(end..<text.size()) !? [])])
}global method trim for TextTypes(self: Self): Self {
return self.trimStart().trimEnd()
}global method trimStart for TextTypes(self: Self): Self {
return Self.fromCodepoints(self.codepoints().dropWhile(λ(c) c.isWhitespace()))
}global method trimEnd for TextTypes(self: Self): Self {
return Self.fromCodepoints(self.codepoints().reverse().dropWhile(λ(c) c.isWhitespace()).reverse())
}global method stripPrefix for TextTypes(self: Self, prefix: Self): Self? {
return self.startsWith(prefix) ? self.slice(prefix.size()..<self.size()) !? undefined : undefined
}global method stripSuffix for TextTypes(self: Self, suffix: Self): Self? {
return self.endsWith(suffix) ? self.slice(0..<(self.size() - suffix.size())) !? undefined : undefined
}global method padStart for TextTypes(self: Self, length: U64, fill: Codepoint := ' '): Self {
let missing := length - self.size().min(length)
return Self.fromCodepoints(Array.filled(missing, fill)) ++ self
}global method padEnd for TextTypes(self: Self, length: U64, fill: Codepoint := ' '): Self {
let missing := length - self.size().min(length)
return self ++ Self.fromCodepoints(Array.filled(missing, fill))
}/// each code point's own mapping - some map to several ("ß" to "SS")
global method toUpper for TextTypes(self: Self): Self {
return Self.fromCodepoints(self.codepoints().flatMap(λ(c) c.toUpper().codepoints()))
}global method toLower for TextTypes(self: Self): Self {
return Self.fromCodepoints(self.codepoints().flatMap(λ(c) c.toLower().codepoints()))
}global method capitalize for TextTypes(self: Self): Self {
let text := self.codepoints()
if text.isEmpty() then { return self }
return Self.fromCodepoints([..(text.first() ?? ' ').toUpper().codepoints(), ..text.drop(1)])
}global method isBlank for TextTypes(self: Self): Boolean {
return self.codepoints().all(λ(c) c.isWhitespace())
}global method isAscii for TextTypes(self: Self): Boolean {
return self.codepoints().all(λ(c) c.isAscii())
} uses #builtin.normalize
/// canonical (NFC, NFD) or compatibility (NFKC, NFKD) form
global method normalize for TextTypes(self: Self, form: NFC | NFD | NFKC | NFKD := NFC): Self {
return Self.fromCodepoints(#builtin.normalize(self.codepoints(), form))
}global method compareIgnoreCase for TextTypes(self: Self, other: Self): Ordering {
return self.toLower().compare(other.toLower())
}global method toNumber for TextTypes(self: Self): ParseError ! Number {
return Number.parse(self as String)
}global method toInteger for TextTypes(self: Self, radix: U8 := 10): ParseError ! Integer {
return Integer.parse(self as String, radix)
}global method parse for TextTypes<Target: FromString>(self: Self): ParseError ! Target {
return Target.parse(self as String)
}global method matches for TextTypes(self: Self, regex: Regex): Array<Match> {
return regex.findAll(self as String).collect()
}/// "{name}" becomes values' name, through its toString(); a placeholder with no value stays as written
global method format for TextTypes(self: Self, values: Map<String, Unknown>): Self {
let text := self.codepoints()
let result: Array<Codepoint> := []
let i: U64 := 0
for _ in forever() while i < text.size() {
let c := text[i] ?? ' '
let close := c == '{' ? (text.slice((i + 1)..<text.size()) !? []).indexOf('}') : undefined
let name := close == undefined ? undefined : String.fromCodepoints(text.slice((i + 1)..<(i + 1 + close)) !? [])
let value := name == undefined ? undefined : values.get(name)
if value == undefined then {
result.push(c)
set i += 1
} else {
set result ++= value.toString().codepoints()
set i += close + 2
}
}
return Self.fromCodepoints(result)
}/// the text ==, hash and compare look at - itself, or its &stringNormalization form when one is injected
local method canonical for TextTypes(self: Self): Self {
return &stringNormalization.map(λ(form) self.normalize(form)) ?? self
}/// Levenshtein's distance - one row of the table at a time, each cell the cheapest of a removal, an
/// insertion and a replacement (free when the codepoints are the same)
global method editDistance for TextTypes(self: Self, other: Self): U64 {
let a := self.codepoints()
let b := other.codepoints()
let previous: Array<U64> := (0..=b.size()).toArray()
for i in 0..<a.size() {
let current: Array<U64> := [i + 1]
for j in 0..<b.size() {
let replaced := previous[j]!! + (a[i]!! == b[j]!! ? 0 : 1)
current.push(replaced.min(previous[j + 1]!! + 1).min(current[j]!! + 1))
}
set previous := current
}
return previous[b.size()]!!
}/// a codepoint that starts a word - the first, one after anything but a letter or digit, or a capital
/// after a lowercase letter (fooBar)
local function wordStartAt for TextTypes(text: Array<Codepoint>, at: U64): Boolean {
if at == 0 then {
return true
}
let before := text[at - 1]!!
return not before.isAlphanumeric() or (text[at]!!.isUpper() and before.isLower())
}/// each of query's codepoints found in order, ignoring case: a point for each, 4 more when it follows
/// the one before at once, 5 more at the start of a word, 2 more when query is the whole text
global method fuzzyScore for TextTypes(self: Self, query: Self): U64? {
let text := self.codepoints()
let lower := text.map(λ(c) c.toLower().codepoints()[0] ?? c)
let score: U64 := 0
let from: U64 := 0
let previous: U64? := undefined
for wanted in query.toLower().codepoints() {
let found := (from..<lower.size()).find(λ(i) lower[i]!! == wanted)
if found == undefined then {
return undefined
}
set score += 1
if previous != undefined and found == previous + 1 then {
set score += 4
}
if wordStartAt(text, found) then {
set score += 5
}
set previous := found
set from := found + 1
}
if query.size() > 0 and query.size() == self.size() then {
set score += 2
}
return score
}global method dedent for TextTypes(self: Self): Self {
let lines := self.replaceAll("\r\n", "\n").split("\n")
let indents := lines.filter(λ(line) not line.isBlank()).map(λ(line) line.size() - line.trimStart().size())
/// the shortest run of whitespace every non-blank line starts with, codepoint for codepoint
let common := indents.isEmpty() ? 0 : indents.min()!!
for line in lines if not line.isBlank() {
for i in 0..<common if line.charAt(i) != lines.find(λ(other) not other.isBlank())!!.charAt(i) {
set common := i
}
}
return lines.map(λ(line) line.isBlank() ? "" : line.slice(common..<line.size())!!).join("\n")
}global method indent for TextTypes(self: Self, prefix: Self := " "): Self {
return self.replaceAll("\r\n", "\n").split("\n").map(λ(line) line.isBlank() ? line : prefix ++ line).join("\n")
}#equatable-for-texttypes/// valid text in one encoding has exactly one byte form, so equal text is equal bytes
method equals(self: Self, other: Self): Boolean {
return self.canonical().units == other.canonical().units
}#hashable-for-texttypesmethod hash(self: Self): U64 {
return hashBytes(self.canonical().units)
}#comparable-for-texttypesmethod compare(self: Self, other: Self): Ordering {
return self.canonical().codepoints().compare(other.canonical().codepoints())
}#tostring-for-texttypesmethod toString(self: Self): String {
return Self == UTF8 ? self : String.fromCodepoints(self.codepoints())
}#iterable-codepoint-for-texttypesmethod iterator(self: Self): Iterator<Codepoint> {
let i: U64 := 0
for _ in forever() while i < self.units.size() {
let (value, width) := Self.decodeAt(self.units, i) ?? (0, 1)
yield Codepoint.fromU32(value) !? '?'
set i += width
}
}#fromiterable-codepoint-for-texttypesfunction fromIterable(items: Iterable<Codepoint>): Self {
return Self.fromCodepoints(items.toArray())
}#collection-codepoint-for-texttypesmethod contains(self: Self, part: Self | Regex): Boolean {
return self.findPart(part, 0) != undefined
}#sized-codepoint-for-texttypesmethod size(self: Self): U64 {
return self.codepoints().size()
}method isEmpty(self: Self): Boolean {
return self.units.isEmpty()
}#indexable-u64-codepoint-for-texttypesmethod get(self: Self, key: U64): Codepoint? {
return self.charAt(key)
}/// re-encodes in place - the replacement may take a different number of bytes
method set(self: Self, key: U64, value: Codepoint): Error ! Undefined {
let text := self.codepoints()
if key >= text.size() then { error IndexError.new("index " ++ key.toString() ++ " is past the end of the text") }
set text[key] := value
set self.units := Self.fromCodepoints(text).units
}#castable-target-for-texttypesone copy per pair of encodings - the text is decoded and encoded again
method cast(self: Self): Target {
if Target == Self then { return self }
if Target == ASCII and not self.isAscii() then { error CastError.new("text has non-ASCII characters") }
return Target.fromCodepoints(self.codepoints())
}#concatenable-for-texttypesmethod concat(self: Self, other: Self): Self {
return Self{units := self.units ++ other.units}
}#default-for-texttypesfunction default(): Self {
return Self{units := Bytes.filled(0)}
}#clone-for-texttypesmethod clone(self: Self): Self {
return Self{units := self.units.clone()}
}#stringbuilderLayout
global function new for StringBuilder(): StringBuilder {
return StringBuilder{parts := [], length := 0}
}global method append for StringBuilder(self: StringBuilder, value: Unknown): StringBuilder {
let text := #format"#{ value }"
self.parts.push(text)
set self.length += text.size()
return self
}global method appendLine for StringBuilder(self: StringBuilder, value: Unknown := ""): StringBuilder {
return self.append(value).append("\n")
}global method appendAll for StringBuilder(self: StringBuilder, values: Iterable<Unknown>, separator: String := ""): StringBuilder {
for value, i in values {
if i > 0 then {
self.append(separator)
}
self.append(value)
}
return self
}global method size for StringBuilder(self: StringBuilder): U64 {
return self.length
}global method isEmpty for StringBuilder(self: StringBuilder): Boolean {
return self.length == 0
}global method clear for StringBuilder(self: StringBuilder): Undefined {
self.parts.clear()
set self.length := 0
}#tostring-for-stringbuilderthe pieces joined once - and kept as one piece, so asking again costs nothing
method toString(self: StringBuilder): String {
let joined := self.parts.join("")
set self.parts := [joined]
return joined
}#default-for-stringbuilderfunction default(): StringBuilder {
return StringBuilder.new()
}#codepoint/// the superscript and subscript letters are their own tokens (xᵢ is x indexed by i), never part of a name
local const scriptLetters := "ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ"/// a binary Unicode property - ID_Start, ID_Continue, ...
local method hasProperty for Codepoint(self: Codepoint, name: String): Boolean {
return #builtin.unicodeProperty(self, name)
} uses #builtin.intrinsic
global function fromU32 for Codepoint(value: U32): CastError ! Codepoint {
return #builtin.intrinsic.CAST_U32_CODEPOINT(value)
} uses #builtin.intrinsic
global method value for Codepoint(self: Codepoint): U32 {
return #builtin.intrinsic.CAST_CODEPOINT_U32(self)
}global method isLetter for Codepoint(self: Codepoint): Boolean {
return self.category().startsWith("L")
}global method isDigit for Codepoint(self: Codepoint, radix: U8 := 10): Boolean {
return self.digitValue(radix) != undefined
}global method isAlphanumeric for Codepoint(self: Codepoint): Boolean {
return self.isLetter() or self.category().startsWith("N")
}/// Unicode's White_Space property - short enough to list
global method isWhitespace for Codepoint(self: Codepoint): Boolean {
let v := self.value()
let singles: Array<U32> := [0x20, 0x85, 0xA0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000]
return (v >= 0x9 and v <= 0xD) or (v >= 0x2000 and v <= 0x200A) or singles.contains(v)
}global method isUpper for Codepoint(self: Codepoint): Boolean {
return self.category() == "Lu"
}global method isLower for Codepoint(self: Codepoint): Boolean {
return self.category() == "Ll"
}global method isPunctuation for Codepoint(self: Codepoint): Boolean {
return self.category().startsWith("P")
}global method isControl for Codepoint(self: Codepoint): Boolean {
return self.category() == "Cc"
}global method isAscii for Codepoint(self: Codepoint): Boolean {
return self.value() < 0x80
}global method isIdStart for Codepoint(self: Codepoint): Boolean {
return (self == '_' or self.hasProperty("ID_Start")) and not scriptLetters.has(self)
}global method isIdContinue for Codepoint(self: Codepoint): Boolean {
return self.hasProperty("ID_Continue") and not scriptLetters.has(self)
}/// any Unicode symbol (category S), plus the ASCII operator characters
global method isSymbol for Codepoint(self: Codepoint): Boolean {
return self.category().startsWith("S") or ":!#$%&*+./<=>?@\\^|~-".has(self)
}/// a String - some code points uppercase to several ("ß" to "SS")
global method toUpper for Codepoint(self: Codepoint): String {
return String.fromCodepoints(#builtin.unicodeUpper(self))
}global method toLower for Codepoint(self: Codepoint): String {
return String.fromCodepoints(#builtin.unicodeLower(self))
}/// 0-9, then a-z / A-Z for 10 and up
global method digitValue for Codepoint(self: Codepoint, radix: U8 := 10): U8? {
let v := self.value()
let digit: U32? := v >= 48 and v <= 57 ? v - 48 : v >= 97 and v <= 122 ? v - 87 : v >= 65 and v <= 90 ? v - 55 : undefined
return digit != undefined and digit < radix as U32 ? digit as U8 : undefined
}global method utf8Length for Codepoint(self: Codepoint): U8 {
let v := self.value()
return v < 0x80 ? 1 : v < 0x800 ? 2 : v < 0x10000 ? 3 : 4
}global method utf16Length for Codepoint(self: Codepoint): U8 {
return self.value() < 0x10000 ? 1 : 2
}/// the Unicode General_Category - "Lu", "Nd", ...
global method category for Codepoint(self: Codepoint): String {
return #builtin.unicodeCategory(self)
}#equatable-for-codepoint uses #builtin.intrinsic
method equals(self: Codepoint, other: Codepoint): Boolean {
return #builtin.intrinsic.EQ_CODEPOINT_CODEPOINT(self, other)
}#hashable-for-codepointmethod hash(self: Codepoint): U64 {
return self.value().hash()
}#comparable-for-codepoint uses #builtin.intrinsic
method compare(self: Codepoint, other: Codepoint): Ordering {
return #builtin.intrinsic.CMP_CODEPOINT_CODEPOINT(self, other)
}#bounded-for-codepointfunction min(): Codepoint {
return '\0'
}function max(): Codepoint {
return '\u{10FFFF}'
}#tostring-for-codepointmethod toString(self: Codepoint): String {
return String.fromCodepoints([self])
}#fromstring-for-codepointfunction parse(text: String): ParseError ! Codepoint {
let all := text.codepoints()
if all.size() != 1 then { error ParseError.new("expected exactly one character, got \"" ++ text ++ "\"") }
return all[0] ?? '?'
}#steppable-for-codepointsteps over the surrogate range, which has no code points
method next(self: Codepoint): Codepoint {
let v := self.value() + 1
return Codepoint.fromU32(v == 0xD800 ? 0xE000 : v) !? self
}method previous(self: Codepoint): Codepoint {
let v := self.value() - 1
return Codepoint.fromU32(v == 0xDFFF ? 0xD7FF : v) !? self
}#castable-target-for-codepointone block each for Target U32 and Target String
method cast(self: Codepoint): Target {
return Target == U32 ? self.value() : self.toString()
}#default-for-codepointfunction default(): Codepoint {
return '\0'
}#copy-for-codepointNothing to write - a marker interface, or one another block already covers.
#bytesa fixed-size run of bytes on &heapAllocator
Layout
local method at for Bytes(self: Bytes, index: U64): U8 {
return self.memory.offset(index as S64).read()
}local method put for Bytes(self: Bytes, index: U64, value: U8): Undefined {
self.memory.offset(index as S64).write(value)
}local method outOfRange for Bytes(self: Bytes, what: String): IndexError {
return IndexError.new(what ++ " is past the end of " ++ self.used.toString() ++ " bytes")
}global function filled for Bytes(size: U64, fill: U8 := 0): Bytes {
let bytes := Bytes{memory := allocate<U8>(size), used := size}
bytes.fill(fill, undefined)
return bytes
}global function fromArray for Bytes(bytes: Array<U8>): Bytes {
let result := Bytes.filled(bytes.size())
for byte, i in bytes {
result.put(i, byte)
}
return result
}global function fromHex for Bytes(text: String): ParseError ! Bytes {
let digits := text.trim()
if digits.size() % 2 != 0 then { error ParseError.new("hex text needs an even number of digits") }
let result := Bytes.filled(digits.size() / 2)
for i in 0..<result.size() {
result.put(i, U8.parse(digits.slice((2 * i)..<(2 * i + 2)) !? "", 16)!!)
}
return result
}/// padding is optional - decoding stops at the first '='
global function fromBase64 for Bytes(text: String): ParseError ! Bytes {
let alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let invalid := ParseError.new("not valid base64")
let sextets: Array<U8> := []
for c in text.trim() until c == '=' {
sextets.push(((alphabet.indexOf(c.toString()) ?! invalid)!!) as U8)
}
if sextets.size() % 4 == 1 then { error invalid }
let result: Array<U8> := []
for chunk in sextets.chunks(4) {
let n := chunk.size()
let word := chunk.reduce(0 as U32, λ(acc, v) (acc shl 6) bor (v as U32)).shl((4 - n) * 6)
for k in 0..<(n - 1) {
result.push((word.shr(16 - 8 * k) band 0xFF) as U8)
}
}
return Bytes.fromArray(result)
}global method slice for Bytes(self: Bytes, range: Range<U64>): IndexError ! Bytes {
let (start, end) := bounds(range)
if end > self.used or start > end then { error self.outOfRange("range " ++ range.toString()) }
let result := Bytes.filled(end - start)
result.copyFrom(self.sliceView(start, end), 0)!!
return result
}/// the same memory, a narrower window - for copying out of without an intermediate copy
local method sliceView for Bytes(self: Bytes, start: U64, end: U64): Bytes {
return Bytes{memory := self.memory.offset(start as S64), used := end - start}
}global method indexOf for Bytes(self: Bytes, part: Bytes, from: U64 := 0): U64? {
for start in from..<(self.used + 1) while start + part.used <= self.used if (0..<part.used).all(λ(k) self.at(start + k) == part.at(k)) {
{ return start }
}
return undefined
}global method startsWith for Bytes(self: Bytes, prefix: Bytes): Boolean {
return prefix.used <= self.used and (0..<prefix.used).all(λ(k) self.at(k) == prefix.at(k))
}global method endsWith for Bytes(self: Bytes, suffix: Bytes): Boolean {
let offset := self.used - suffix.used.min(self.used)
return suffix.used <= self.used and (0..<suffix.used).all(λ(k) self.at(offset + k) == suffix.at(k))
}/// an empty separator doesn't split at all
global method split for Bytes(self: Bytes, separator: Bytes): Array<Bytes> {
if separator.isEmpty() then { return [self.clone()] }
let parts: Array<Bytes> := []
let from: U64 := 0
for _ in forever() {
let found := self.indexOf(separator, from)
if found == undefined then {
parts.push(self.slice(from..<self.used) !? Bytes.filled(0))
return parts
}
parts.push(self.slice(from..<found) !? Bytes.filled(0))
set from := found + separator.used
}
}global method fill for Bytes(self: Bytes, value: U8, range: Range<U64>?): Undefined {
let (start, end) := range == undefined ? (0, self.used) : bounds(range)
for i in start..<end.min(self.used) {
self.put(i, value)
}
}global method copyFrom for Bytes(self: Bytes, source: Bytes, at: U64 := 0): IndexError ! Undefined {
if at + source.used > self.used then { error self.outOfRange("writing " ++ source.used.toString() ++ " bytes at " ++ at.toString()) }
for i in 0..<source.used {
self.put(at + i, source.at(i))
}
}global method reverse for Bytes(self: Bytes): Bytes {
let result := Bytes.filled(self.used)
for i in 0..<self.used {
result.put(self.used - 1 - i, self.at(i))
}
return result
}global method toArray for Bytes(self: Bytes): Array<U8> {
return Array.generate(self.used, λ(i) self.at(i))
}global method toHex for Bytes(self: Bytes): String {
return self.toArray().map(λ(byte) byte.toString(16).padStart(2, '0')).join()
}global method toBase64 for Bytes(self: Bytes): String {
let alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".codepoints()
let result: Array<Codepoint> := []
for chunk in self.toArray().chunks(3) {
let word := chunk.reduce(0 as U32, λ(acc, byte) (acc shl 8) bor (byte as U32)).shl((3 - chunk.size()) * 8)
for k in 0..<4 {
result.push(k <= chunk.size() ? alphabet[(word.shr(18 - 6 * k) band 0x3F) as U64] ?? '=' : '=')
}
}
return String.fromCodepoints(result)
}global method decode for Bytes<Encoding: UTF8 | UTF16 | UTF32 | ASCII>(self: Bytes): ParseError ! Encoding {
return Encoding.decode(self)
}global method readInteger for Bytes<Target>(self: Bytes, at: U64, endian: Little | Big := Little): IndexError ! Target {
let width := (Target.bits() / 8) as U64
let raw := self.slice(at..<(at + width))!!
return Target.fromBytes(endian == Big ? raw.reverse() : raw) !? 0
}global method readFloat for Bytes<Target>(self: Bytes, at: U64, endian: Little | Big := Little): IndexError ! Target {
if Target == F16 then { return F16.fromBits(self.readInteger<U16>(at, endian)!!) }
if Target == F32 then { return F32.fromBits(self.readInteger<U32>(at, endian)!!) }
return F64.fromBits(self.readInteger<U64>(at, endian)!!)
}global method writeInteger for Bytes<Source>(self: Bytes, at: U64, value: Source, endian: Little | Big := Little): IndexError ! Undefined {
let raw := value.toBytes()
self.copyFrom(endian == Big ? raw.reverse() : raw, at)!!
}global method writeFloat for Bytes<Source>(self: Bytes, at: U64, value: Source, endian: Little | Big := Little): IndexError ! Undefined {
self.writeInteger(at, value.toBits(), endian)!!
}global method toBase64Url for Bytes(self: Bytes): String {
return self.toBase64().replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "")
}global function fromBase64Url for Bytes(text: String): ParseError ! Bytes {
if text.contains("+") or text.contains("/") then {
error ParseError.new("not base64url: " ++ text)
}
return Bytes.fromBase64(text.trim().replaceAll("-", "+").replaceAll("_", "/"))!!
}/// what CRC-32 does to each byte value, eight bits at a time - worked out once
local const crcTable: Array<U32> := crcTableMade()local function crcTableMade for Bytes(): Array<U32> {
let table: Array<U32> := []
for value in 0..<256 {
let crc := value as U32
for _ in 0..<8 {
set crc := crc band 1 == 1 ? crc shr 1 bxor 0xEDB88320 : crc shr 1
}
table.push(crc)
}
return table
}global method crc32 for Bytes(self: Bytes): U32 {
let crc: U32 := 0xFFFFFFFF
for byte in self {
set crc := crcTable[((crc bxor byte as U32) band 0xFF) as U64]!! bxor (crc shr 8)
}
return crc bxor 0xFFFFFFFF
}global method fnv1a for Bytes(self: Bytes): U64 {
return hashBytes(self, 0xcbf29ce484222325)
}/// the message padded to whole 64-byte blocks - a 1 bit, zeros, then its length in bits in 8 bytes,
/// big-endian for SHA, little-endian for MD5
local method padded for Bytes(self: Bytes, endian: Little | Big): Bytes {
let length := Bytes.filled(8)
length.writeInteger<U64>(0, self.size() * 8, endian)!!
let zeros := (55 - self.size() % 64 + 64) % 64
return self ++ Bytes.fromArray([0x80]) ++ Bytes.filled(zeros) ++ length
}local const sha256Rounds: Array<U32> := [/// FIPS 180-4's SHA-256 - U32 arithmetic wraps with +%
global method sha256 for Bytes(self: Bytes): Bytes {
let data := self.padded(Big)
let h: Array<U32> := [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
for block in (0..<data.size()).step(64) {
let w: Array<U32> := (0..<16).map(λ(t) data.readInteger<U32>(block + t * 4, Big)!!).toArray()
for t in 16..<64 {
let s0 := w[t - 15]!!.rotateRight(7) bxor w[t - 15]!!.rotateRight(18) bxor (w[t - 15]!! shr 3)
let s1 := w[t - 2]!!.rotateRight(17) bxor w[t - 2]!!.rotateRight(19) bxor (w[t - 2]!! shr 10)
w.push(w[t - 16]!! +% s0 +% w[t - 7]!! +% s1)
}
let (a, b, c, d, e, f, g, k) := (h[0]!!, h[1]!!, h[2]!!, h[3]!!, h[4]!!, h[5]!!, h[6]!!, h[7]!!)
for t in 0..<64 {
let first := k +% (e.rotateRight(6) bxor e.rotateRight(11) bxor e.rotateRight(25)) +% ((e band f) bxor (bnot e band g)) +% sha256Rounds[t]!! +% w[t]!!
let second := (a.rotateRight(2) bxor a.rotateRight(13) bxor a.rotateRight(22)) +% ((a band b) bxor (a band c) bxor (b band c))
set (k, g, f, e, d, c, b, a) := (g, f, e, d +% first, c, b, a, first +% second)
}
set h := [h[0]!! +% a, h[1]!! +% b, h[2]!! +% c, h[3]!! +% d, h[4]!! +% e, h[5]!! +% f, h[6]!! +% g, h[7]!! +% k]
}
return Bytes.fromIterable(h.flatMap(λ(word) word.toBytes().reverse()))
}/// FIPS 180-4's SHA-1
global method sha1 for Bytes(self: Bytes): Bytes {
let data := self.padded(Big)
let h: Array<U32> := [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
for block in (0..<data.size()).step(64) {
let w: Array<U32> := (0..<16).map(λ(t) data.readInteger<U32>(block + t * 4, Big)!!).toArray()
for t in 16..<80 {
w.push((w[t - 3]!! bxor w[t - 8]!! bxor w[t - 14]!! bxor w[t - 16]!!).rotateLeft(1))
}
let (a, b, c, d, e) := (h[0]!!, h[1]!!, h[2]!!, h[3]!!, h[4]!!)
for t in 0..<80 {
let (f, constant) := t < 20 ? ((b band c) bor (bnot b band d), 0x5A827999 as U32) : t < 40 ? (b bxor c bxor d, 0x6ED9EBA1 as U32) : t < 60 ? ((b band c) bor (b band d) bor (c band d), 0x8F1BBCDC as U32) : (b bxor c bxor d, 0xCA62C1D6 as U32)
let next := a.rotateLeft(5) +% f +% e +% constant +% w[t]!!
set (e, d, c, b, a) := (d, c, b.rotateLeft(30), a, next)
}
set h := [h[0]!! +% a, h[1]!! +% b, h[2]!! +% c, h[3]!! +% d, h[4]!! +% e]
}
return Bytes.fromIterable(h.flatMap(λ(word) word.toBytes().reverse()))
}/// each MD5 round's left rotation
local const md5Shifts: Array<U8> := [7, 12, 17, 22].repeat(4) ++ [5, 9, 14, 20].repeat(4) ++ [4, 11, 16, 23].repeat(4) ++ [6, 10, 15, 21].repeat(4)/// floor(2^32 * abs(sin(i + 1))) for each round
local const md5Constants: Array<U32> := (0..<64).map(λ(i) ((i + 1) as F64).sin().abs() * 4294967296.0).map(λ(x) x.floor() as U32).toArray()/// RFC 1321's MD5 - little-endian throughout
global method md5 for Bytes(self: Bytes): Bytes {
let data := self.padded(Little)
let h: Array<U32> := [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
for block in (0..<data.size()).step(64) {
let m: Array<U32> := (0..<16).map(λ(t) data.readInteger<U32>(block + t * 4, Little)!!).toArray()
let (a, b, c, d) := (h[0]!!, h[1]!!, h[2]!!, h[3]!!)
for i in 0..<64 {
let (f, g) := i < 16 ? ((b band c) bor (bnot b band d), i) : i < 32 ? ((d band b) bor (bnot d band c), (5 * i + 1) % 16) : i < 48 ? (b bxor c bxor d, (3 * i + 5) % 16) : (c bxor (b bor bnot d), (7 * i) % 16)
let mixed := a +% f +% md5Constants[i]!! +% m[g]!!
set (a, d, c, b) := (d, c, b, b +% mixed.rotateLeft(md5Shifts[i]!!))
}
set h := [h[0]!! +% a, h[1]!! +% b, h[2]!! +% c, h[3]!! +% d]
}
return Bytes.fromIterable(h.flatMap(λ(word) word.toBytes()))
}/// RFC 2104 - the key padded to SHA-256's 64-byte block (hashed first when longer), then
/// sha256((key bxor 0x5c...) ++ sha256((key bxor 0x36...) ++ message))
global method hmacSha256 for Bytes(self: Bytes, key: Bytes): Bytes {
let block := key.size() > 64 ? key.sha256() : key
let padded := block ++ Bytes.filled(64 - block.size())
let inner := Bytes.fromIterable(padded.map(λ(b) b bxor 0x36))
let outer := Bytes.fromIterable(padded.map(λ(b) b bxor 0x5c))
return (outer ++ (inner ++ self).sha256()).sha256()
}global method compress for Bytes(self: Bytes, format: Compression := Compression.Gzip): Bytes {
let deflated := Bytes.fromArray(deflate(self.toArray()))
match format {
case Compression.Deflate then {
return deflated
}
case Compression.Zlib then {
let checksum := Bytes.filled(4)
checksum.writeInteger<U32>(0, adler32(self), Big)!!
return Bytes.fromArray([0x78, 0x5e]) ++ deflated ++ checksum
}
case Compression.Gzip then {
let trailer := Bytes.filled(8)
trailer.writeInteger<U32>(0, self.crc32(), Little)!!
trailer.writeInteger<U32>(4, (self.size() % 4294967296) as U32, Little)!!
return Bytes.fromArray([0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255]) ++ deflated ++ trailer
}
}
}global method decompress for Bytes(self: Bytes, format: Compression := Compression.Gzip): ParseError ! Bytes {
match format {
case Compression.Deflate then {
/// zlib's own reader, given a zlib header to read past
return #builtin.zlibDecompress(Bytes.fromArray([0x78, 0x01]) ++ self)!!
}
case Compression.Zlib then {
return #builtin.zlibDecompress(self)!!
}
case Compression.Gzip then {
if self.size() < 18 or self[0] != 0x1f or self[1] != 0x8b or self[2] != 8 then {
error ParseError.new("not gzip data")
}
/// past the header's optional extra field, file name, comment and header checksum
let flags := self[3]!!
let at: U64 := 10
if flags band 4 != 0 then {
set at += 2 + self.readInteger<U16>(at, Little)!! as U64
}
for flag in [8 as U8, 16 as U8] if flags band flag != 0 {
set at := (self.indexOf(Bytes.filled(1), at) ?! ParseError.new("the gzip header ends too soon")) + 1
}
if flags band 2 != 0 then {
set at += 2
}
/// the last 8 bytes are the data's CRC-32 and size
let end := self.size() - 8
let data := Bytes.fromArray([0x78, 0x01]) ++ self.slice(at..<end)!!
let inflated := #builtin.zlibDecompress(data)!!
if self.readInteger<U32>(end, Little)!! != inflated.crc32() then {
error ParseError.new("gzip data whose checksum doesn't match")
}
return inflated
}
}
}/// RFC 1950's Adler-32 - two sums modulo 65521
local function adler32 for Bytes(data: Bytes): U32 {
let (a, b) := (1 as U32, 0 as U32)
for byte in data {
set a := (a + byte as U32) % 65521
set b := (b + a) % 65521
}
return (b shl 16) bor a
}/// one fixed-Huffman DEFLATE block (RFC 1951's 3.2.6): each position's longest match of 3 to 258 bytes up
/// to 32 KiB back - found through a chain of the earlier positions whose next 3 bytes hash alike - as a
/// length and a distance, a literal byte where there's none
local function deflate for Bytes(data: Array<U8>): Array<U8> {
let writer := BitWriter.new()
writer.putBits(1, 1)
writer.putBits(1, 2)
let latest: Map<U64, U64> := Map.of()
let previous: Map<U64, U64> := Map.of()
let at: U64 := 0
for _ in forever() while at < data.size() {
let best := (length := 0 as U64, distance := 0 as U64)
let candidate := at + 3 <= data.size() ? latest[tripleHash(data, at)] : undefined
for _ in 0..<48 while candidate != undefined and at - candidate < 32768 {
let length := (0..<(data.size() - at).min(258)).takeWhile(λ(k) data[candidate + k] == data[at + k]).count(λ(k) true)
if length > best.length then {
set best := (length := length, distance := at - candidate)
}
set candidate := previous[candidate]
}
let step := best.length >= 3 ? best.length : 1
if best.length >= 3 then {
writer.putMatch(best.length, best.distance)
} else {
writer.putFixed(data[at]!! as U64)
}
for p in at..<at + step if p + 3 <= data.size() {
let hash := tripleHash(data, p)
if latest.has(hash) then {
previous.set(p, latest[hash]!!)
}
latest.set(hash, p)
}
set at += step
}
writer.putFixed(256)
return writer.finish()
}local function tripleHash for Bytes(data: Array<U8>, at: U64): U64 {
return ((data[at]!! as U64 shl 10) bxor (data[at + 1]!! as U64 shl 5) bxor data[at + 2]!! as U64) band 0x7FFF
}local function new for BitWriter(): BitWriter {
return BitWriter{out := [], buffer := 0, count := 0}
}local method putBits for BitWriter(self: BitWriter, value: U64, bits: U64): Undefined {
set self.buffer := self.buffer bor (value shl self.count)
set self.count += bits
for _ in forever() while self.count >= 8 {
self.out.push((self.buffer band 0xFF) as U8)
set self.buffer := self.buffer shr 8
set self.count -= 8
}
}/// a Huffman code - written most significant bit first, so reversed
local method putCode for BitWriter(self: BitWriter, code: U64, bits: U64): Undefined {
self.putBits((0..<bits).reduce(0 as U64, λ(out, i) (out shl 1) bor ((code shr i) band 1)), bits)
}/// a literal or length symbol in the fixed code
local method putFixed for BitWriter(self: BitWriter, symbol: U64): Undefined {
if symbol < 144 then {
self.putCode(0x30 + symbol, 8)
} else if symbol < 256 then {
self.putCode(0x190 + symbol - 144, 9)
} else if symbol < 280 then {
self.putCode(symbol - 256, 7)
} else {
self.putCode(0xC0 + symbol - 280, 8)
}
}local const lengthBases: Array<U64> := [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258]local const lengthExtras: Array<U64> := [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]local const distanceBases: Array<U64> := [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577]local const distanceExtras: Array<U64> := [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13]local method putMatch for BitWriter(self: BitWriter, length: U64, distance: U64): Undefined {
let l := lengthBases.lastIndexWhere(λ(base) base <= length)!!
self.putFixed(257 + l)
self.putBits(length - lengthBases[l]!!, lengthExtras[l]!!)
let d := distanceBases.lastIndexWhere(λ(base) base <= distance)!!
self.putCode(d, 5)
self.putBits(distance - distanceBases[d]!!, distanceExtras[d]!!)
}/// the last partial byte, padded with zeros
local method finish for BitWriter(self: BitWriter): Array<U8> {
if self.count > 0 then {
self.out.push((self.buffer band 0xFF) as U8)
}
return self.out
}#equatable-for-bytesmethod equals(self: Bytes, other: Bytes): Boolean {
return self.used == other.used and (0..<self.used).all(λ(i) self.at(i) == other.at(i))
}#hashable-for-bytesmethod hash(self: Bytes): U64 {
return hashBytes(self)
}#comparable-for-bytes/// the first differing byte decides; a prefix sorts first
method compare(self: Bytes, other: Bytes): Ordering {
for i in 0..<self.used.min(other.used) {
let order := self.at(i).compare(other.at(i))
if order != Ordering.Equal then { return order }
}
return self.used.compare(other.used)
}#tostring-for-bytesmethod toString(self: Bytes): String {
return self.toHex()
}#iterable-u8-for-bytesmethod iterator(self: Bytes): Iterator<U8> {
for i in 0..<self.used {
yield self.at(i)
}
}#fromiterable-u8-for-bytesfunction fromIterable(items: Iterable<U8>): Bytes {
return Bytes.fromArray(items.toArray())
}#collection-u8-for-bytesmethod contains(self: Bytes, item: U8): Boolean {
return (0..<self.used).any(λ(i) self.at(i) == item)
}#sized-u8-for-bytesmethod size(self: Bytes): U64 {
return self.used
}method isEmpty(self: Bytes): Boolean {
return self.used == 0
}#indexable-u64-u8-for-bytesmethod get(self: Bytes, key: U64): U8? {
return key < self.used ? self.at(key) : undefined
}method set(self: Bytes, key: U64, value: U8): Error ! Undefined {
if key >= self.used then { error self.outOfRange("index " ++ key.toString()) }
self.put(key, value)
}#concatenable-for-bytesmethod concat(self: Bytes, other: Bytes): Bytes {
let result := Bytes.filled(self.used + other.used)
result.copyFrom(self, 0)!!
result.copyFrom(other, self.used)!!
return result
}#default-for-bytesfunction default(): Bytes {
return Bytes.filled(0)
}#clone-for-bytesmethod clone(self: Bytes): Bytes {
let copy := Bytes.filled(self.used)
copy.copyFrom(self, 0) !? undefined
return copy
}#compressionNo members of its own.
#equatable-for-compressionmethod equals(self: Compression, other: Compression): Boolean {
return self.index() == other.index()
}#hashable-for-compressionmethod hash(self: Compression): U64 {
return self.index().hash()
}#tostring-for-compressionits name alone - "Gzip"
method toString(self: Compression): String {
return self.variantName()
}#copy-for-compressionNothing to write - a marker interface, or one another block already covers.
#jsonglobal function parse for Json(text: String): ParseError ! Unknown {
let reader := DocumentReader.of(text)
let value := reader.jsonValue()!!
reader.skipSpace()
if not reader.atEnd() then {
error reader.failure("JSON", "something after the document")
}
return value
}global function decode for Json<Target>(text: String): ParseError ! Target {
return decoded<Target>(Json.parse(text)!!, "the document")!!
}global function encode for Json(value: Unknown, indent: U64 := 0): String {
return writeJson(value, " ".repeat(indent), "")
}#tostring-for-jsonmethod toString(self: Json): String {
return Json.encode(self)
}#csv/// every row's fields - RFC 4180: a quoted field may hold separators, line breaks and quotes written twice
global function parse for Csv(text: String, separator: Codepoint := ','): ParseError ! Array<Array<String>> {
let rows: Array<Array<String>> := []
let row: Array<String> := []
let field: Array<Codepoint> := []
let (quoted, wasQuoted, line) := (false, false, 1 as U64)
let chars := text.codepoints()
let i: U64 := 0
for _ in forever() while i < chars.size() {
let c := chars[i]!!
set i += 1
if quoted then {
if c == '"' and chars[i] == '"' then {
field.push('"')
set i += 1
} else if c == '"' then {
set quoted := false
} else {
set line += c == '\n' ? 1 : 0
field.push(c)
}
} else if c == '"' then {
if not field.isEmpty() then {
error ParseError.new("not CSV - a quote in the middle of a field, on line " ++ line.toString())
}
set (quoted, wasQuoted) := (true, true)
} else if c == separator then {
row.push(String.fromCodepoints(field))
set (field, wasQuoted) := ([], false)
} else if c == '\n' then {
row.push(String.fromCodepoints(field))
rows.push(row)
set (row, field, wasQuoted) := ([], [], false)
set line += 1
} else if c != '\r' or chars[i] != '\n' then {
if wasQuoted then {
error ParseError.new("not CSV - text after a quoted field's closing quote, on line " ++ line.toString())
}
field.push(c)
}
}
if quoted then {
error ParseError.new("not CSV - a quoted field that never ends, on line " ++ line.toString())
}
if not field.isEmpty() or not row.isEmpty() or wasQuoted then {
row.push(String.fromCodepoints(field))
rows.push(row)
}
return rows
}global function records for Csv(text: String, separator: Codepoint := ','): ParseError ! Array<Map<String, String>> {
let rows := Csv.parse(text, separator)!!
let header := rows[0] ?? []
return rows.drop(1).enumerate().map(λ((i, row)) {
if row.size() != header.size() then {
error ParseError.new("not CSV - " ++ row.size().toString() ++ " field(s) where the header has " ++ header.size().toString() ++ ", on line " ++ (i + 2).toString())
}
return Map.fromIterable(header.zip(row))
}).toArray()
} uses #runtime.construct
/// each record's fields by name into a Row - an empty field undefined unless the field is text
global function decode for Csv<Row>(text: String, separator: Codepoint := ','): ParseError ! Table<Row> {
let fields := Type.of<Row>().fields()
let rows := Csv.records(text, separator)!!.enumerate().map(λ((i, record)) {
let values := fields.entries().map(λ((name, type)) {
let cell := record[name]
return (name, decodedAs(cell == "" and type.name() != "String" ? undefined : cell, type.name(), "row " ++ (i + 1).toString() ++ "'s " ++ name)!!)
})
return #runtime.construct(Type.of<Row>(), Map.fromIterable(values))!! as Row
})
return Table.fromRows(rows)
}global function write for Csv(rows: Iterable<Iterable<Unknown>>, separator: Codepoint := ','): String {
let mark := separator.toString()
let field := λ(value: Unknown) {
let text := value == undefined ? "" : value.toString()
return text.contains(mark) or text.contains("\"") or text.contains("\n") or text.contains("\r") ? "\"" ++ text.replaceAll("\"", "\"\"") ++ "\"" : text
}
return rows.map(λ(row) row.map(field).join(mark) ++ "\n").join("")
}/// a header row of Row's fields, then each row's fields in that order
global function encode for Csv<Row>(rows: Table<Row>, separator: Codepoint := ','): String {
let names := rows.columns()
return Csv.write([names, ..rows.toArray().map(λ(row) names.map(λ(name) row.get(name)))], separator)
}#tomlglobal function parse for Toml(text: String): ParseError ! Map<String, Unknown> {
let reader := DocumentReader.of(text)
let root: Map<String, Unknown> := Map.of()
let current := root
for _ in forever() {
reader.tomlSpace()
if reader.atEnd() then {
return root
}
if reader.take("[[") then {
/// [[a.b]] - one more table in the array a.b
let path := reader.tomlKey()!!
if not reader.take("]]") then {
error reader.failure("TOML", "expected ']]'")
}
let made: Map<String, Unknown> := Map.of()
let list := reader.tomlTable(root, path.dropLast(1))!!.getOrInsert(path.last()!!, λ [] as Array<Unknown>)
list.narrow<Array<Unknown>>()?.push(made) ?! reader.failure("TOML", path.join(".") ++ " isn't an array of tables")
set current := made
} else if reader.take("[") then {
let path := reader.tomlKey()!!
if not reader.take("]") then {
error reader.failure("TOML", "expected ']'")
}
set current := reader.tomlTable(root, path)!!
} else {
let key := reader.tomlKey()!!
if not reader.take("=") then {
error reader.failure("TOML", "expected '='")
}
reader.tomlPut(current, key, reader.tomlValue()!!)!!
}
reader.tomlBlank()
if not reader.atEnd() and reader.peek() != '\n' and reader.peek() != '\r' then {
error reader.failure("TOML", "expected the end of the line")
}
}
}global function decode for Toml<Target>(text: String): ParseError ! Target {
return decoded<Target>(Toml.parse(text)!!, "the document")!!
}/// spaces and a comment, up to the end of the line
local method tomlBlank for DocumentReader(self: DocumentReader): Undefined {
for _ in forever() while self.peek() == ' ' | '\t' {
set self.at += 1
}
if self.peek() == '#' then {
for _ in forever() while self.peek() != undefined and self.peek() != '\n' {
set self.at += 1
}
}
}/// blank lines and comments too
local method tomlSpace for DocumentReader(self: DocumentReader): Undefined {
for _ in forever() {
self.tomlBlank()
if not (self.take("\n") or self.take("\r")) then {
return undefined
}
}
}/// a dotted key's parts - a.b."c d"
local method tomlKey for DocumentReader(self: DocumentReader): ParseError ! Array<String> {
let parts: Array<String> := []
for _ in forever() {
self.tomlBlank()
let part := self.peek() == '"' ? self.tomlBasicString()!! : self.peek() == '\'' ? self.tomlLiteralString()!! : String.fromCodepoints(self.takeWhile(λ(c) c.isAscii() and c.isAlphanumeric() or c == '_' | '-'))
if part == "" then {
error self.failure("TOML", "expected a key")
}
parts.push(part)
self.tomlBlank()
if not self.take(".") then {
return parts
}
}
}local method takeWhile for DocumentReader(self: DocumentReader, where: Lambda<Codepoint, Boolean>): Array<Codepoint> {
let start := self.at
for _ in forever() while self.peek()?.map(where) == true {
set self.at += 1
}
return self.text.slice(start..<self.at)!!
}/// the table a path of keys leads to from root - each made when missing; an array of tables' last
local method tomlTable for DocumentReader(self: DocumentReader, root: Map<String, Unknown>, path: Array<String>): ParseError ! Map<String, Unknown> {
let at := root
for part in path {
let next := at.getOrInsert(part, λ Map<String, Unknown>.of())
set at := next.narrow<Map<String, Unknown>>() ?? next.narrow<Array<Unknown>>()?.last()?.narrow<Map<String, Unknown>>() ?! self.failure("TOML", part ++ " is already a value, not a table")
}
return at
}local method tomlPut for DocumentReader(self: DocumentReader, table: Map<String, Unknown>, key: Array<String>, value: Unknown): ParseError ! Undefined {
let at := self.tomlTable(table, key.dropLast(1))!!
if at.has(key.last()!!) then {
error self.failure("TOML", key.join(".") ++ " is set twice")
}
at.set(key.last()!!, value)
}local method tomlValue for DocumentReader(self: DocumentReader): ParseError ! Unknown {
self.tomlBlank()
match self.peek() {
case '"' then {
return self.tomlBasicString()!!
}
case '\'' then {
return self.tomlLiteralString()!!
}
case '[' then {
set self.at += 1
let items: Array<Unknown> := []
for _ in forever() {
self.tomlSpace()
if self.take("]") then {
return items
}
items.push(self.tomlValue()!!)
self.tomlSpace()
if not self.take(",") and self.peek() != ']' then {
error self.failure("TOML", "expected ',' or ']'")
}
}
}
case '{' then {
set self.at += 1
let table: Map<String, Unknown> := Map.of()
self.tomlBlank()
if self.take("}") then {
return table
}
for _ in forever() {
let key := self.tomlKey()!!
if not self.take("=") then {
error self.failure("TOML", "expected '='")
}
self.tomlPut(table, key, self.tomlValue()!!)!!
self.tomlBlank()
if self.take("}") then {
return table
}
if not self.take(",") then {
error self.failure("TOML", "expected ',' or '}'")
}
}
}
case _ then {
return self.tomlBare()!!
}
}
}/// a basic string's escapes - \uXXXX and \UXXXXXXXX too
local method tomlEscape for DocumentReader(self: DocumentReader): ParseError ! Codepoint {
let escaped := self.peek() ?! self.failure("TOML", "a string that never ends")
set self.at += 1
if escaped == 'u' | 'U' then {
return Codepoint.fromU32(self.hex(escaped == 'u' ? 4 : 8)!!) !? (error self.failure("TOML", "an escape that's no codepoint"))
}
let index := "\"\\bfnrte".indexOf(escaped.toString()) ?! self.failure("TOML", "'\\" ++ escaped.toString() ++ "' isn't an escape")
return ['"', '\\', '\u{8}', '\u{c}', '\n', '\r', '\t', '\u{1b}'][index]!!
}/// "..." or """...""" - a multi-line one drops a line break right after its opening quotes, and a
/// backslash ending a line drops the break and the whitespace after it
local method tomlBasicString for DocumentReader(self: DocumentReader): ParseError ! String {
let out: Array<Codepoint> := []
let multiLine := self.take("\"\"\"")
if multiLine then {
self.take("\r")
self.take("\n")
} else {
set self.at += 1
}
for _ in forever() {
if multiLine and self.take("\"\"\"") then {
/// up to two more quotes belong to the string
for _ in 0..<2 if self.take("\"") {
out.push('"')
}
return String.fromCodepoints(out)
}
let c := self.peek() ?! self.failure("TOML", "a string that never ends")
set self.at += 1
if not multiLine and c == '"' then {
return String.fromCodepoints(out)
}
if not multiLine and c == '\n' then {
error self.failure("TOML", "a string that never ends")
}
if c != '\\' then {
out.push(c)
} else if multiLine and self.peek() == ' ' | '\t' | '\n' | '\r' then {
self.takeWhile(λ(w) w == ' ' | '\t' | '\n' | '\r')
} else {
out.push(self.tomlEscape()!!)
}
}
}/// '...' or '''...''' - no escapes
local method tomlLiteralString for DocumentReader(self: DocumentReader): ParseError ! String {
let multiLine := self.take("'''")
if multiLine then {
self.take("\n")
} else {
set self.at += 1
}
let out: Array<Codepoint> := []
for _ in forever() {
if multiLine and self.take("'''") then {
for _ in 0..<2 if self.take("'") {
out.push('\'')
}
return String.fromCodepoints(out)
}
let c := self.peek() ?! self.failure("TOML", "a string that never ends")
set self.at += 1
if not multiLine and c == '\'' then {
return String.fromCodepoints(out)
}
if not multiLine and c == '\n' then {
error self.failure("TOML", "a string that never ends")
}
out.push(c)
}
}/// a number, boolean, date or time written bare - up to a separator; a space only inside a date-time
local method tomlBare for DocumentReader(self: DocumentReader): ParseError ! Unknown {
let start := self.at
for _ in forever() while self.peek() != undefined and not "\n\r,]}#".contains(self.peek()!!.toString()) and (self.peek() != ' ' or (self.at - start == 10 and self.peek(1)?.isDigit() == true)) {
set self.at += 1
}
let word := String.fromCodepoints(self.text.slice(start..<self.at)!!)
let bad := λ(what: String) self.failure("TOML", word ++ " isn't " ++ what)
match word {
case "true" then { return true }
case "false" then { return false }
case "inf" | "+inf" then { return F64.infinity() }
case "-inf" then { return -F64.infinity() }
case "nan" | "+nan" | "-nan" then { return F64.nan() }
case "" then { error self.failure("TOML", "expected a value") }
case _ then {}
}
if word.size() >= 8 and word.charAt(2) == ':' then {
return TimeOfDay.parse(word) !? (error bad("a time"))
}
if word.size() >= 10 and word.charAt(4) == '-' and word.charAt(7) == '-' then {
let iso := word.replace(" ", "T")
if iso.size() == 10 then {
return Date.parse(iso) !? (error bad("a date"))
}
let zoned := iso.endsWith("Z") or iso.endsWith("z") or iso.slice(19..<iso.size())!!.contains(#regex"[+-]")
return zoned ? Instant.parse(iso) !? (error bad("a date-time")) : DateTime.parse(iso) !? (error bad("a date-time"))
}
if word.contains("__") or word.startsWith("_") or word.endsWith("_") then {
error bad("a number - a misplaced _")
}
let digits := word.replaceAll("_", "")
for (prefix, radix) in [("0x", 16 as U8), ("0o", 8 as U8), ("0b", 2 as U8)] if digits.startsWith(prefix) {
return digits.slice(2..<digits.size())!!.toInteger(radix) !? (error bad("a number"))
}
if digits.contains(#regex"[.eE]") then {
return digits.toNumber() !? (error bad("a number"))
}
if digits.stripPrefix("+")?.startsWith("0") == true and digits.size() > 2 or digits.stripPrefix("-")?.startsWith("0") == true and digits.size() > 2 or digits.startsWith("0") and digits.size() > 1 then {
error bad("a number - a leading zero")
}
return digits.stripPrefix("+")?.toInteger() ?? digits.toInteger() !? (error bad("a value"))
}#yaml/// the first document's lines that hold something - their indents, texts without the indent and
/// comments, numbers - read as blocks by indentation
global function parse for Yaml(text: String): ParseError ! Unknown {
let raw := text.replaceAll("\r\n", "\n").split("\n")
let lines: Array<YamlLine> := []
let started := false
for full, i in raw {
if full.startsWith("---") then {
if started then {
break full
}
set started := true
continue full
}
if full.startsWith("...") then {
break full
}
if full.startsWith("%") then {
continue full
}
if full.slice(0..<full.size() - full.trimStart().size())!!.contains("\t") then {
error yamlFailure(i + 1, "a tab in the indentation")
}
let content := uncommented(full)
if content.isBlank() then {
continue full
}
set started := true
lines.push(YamlLine{indent := content.size() - content.trimStart().size(), text := content.trimStart(), number := i + 1})
}
if lines.isEmpty() then {
return undefined
}
let reader := YamlReader{lines, at := 0, raw}
let value := reader.block()!!
if reader.lines[reader.at] != undefined then {
error yamlFailure(reader.lines[reader.at]!!.number, "a line that doesn't fit where it's indented")
}
return value
}global function decode for Yaml<Target>(text: String): ParseError ! Target {
return decoded<Target>(Yaml.parse(text)!!, "the document")!!
}local function yamlFailure for Yaml(line: U64, what: String): ParseError {
return ParseError.new("not YAML (or not the YAML this reads) - " ++ what ++ ", on line " ++ line.toString())
}/// the text before a # that starts a comment - one after whitespace, outside quotes
local function uncommented for Yaml(text: String): String {
let quote: Codepoint? := undefined
for c, i in text.codepoints() {
if quote == undefined and c == '#' and (i == 0 or text.charAt(i - 1)!!.isWhitespace()) then {
return text.slice(0..<i)!!.trimEnd()
}
if quote == undefined and c == '"' | '\'' then {
set quote := c
} else if c == quote then {
set quote := undefined
}
}
return text.trimEnd()
}/// "key: value" as (key, value) - the first ": ", or a ":" ending the text, outside quotes and brackets
local function splitKey for Yaml(text: String): (String, String)? {
let quote: Codepoint? := undefined
let depth := 0
for c, i in text.codepoints() {
if quote != undefined then {
if c == quote then {
set quote := undefined
}
} else if c == '"' | '\'' then {
set quote := c
} else if c == '[' | '{' then {
set depth += 1
} else if c == ']' | '}' then {
set depth -= 1
} else if c == ':' and depth == 0 and (i + 1 == text.size() or text.charAt(i + 1) == ' ') then {
return (text.slice(0..<i)!!.trim(), text.slice(i + 1..<text.size())!!.trim())
}
}
return undefined
}/// a plain or quoted scalar - YAML 1.2's core schema: null ~, true false, integers, floats, the rest text
local function scalar for Yaml(text: String, line: U64): ParseError ! Unknown {
let word := text.trim()
if word.startsWith("\"") then {
return Json.parse(word) !? (error yamlFailure(line, word ++ " isn't a quoted string"))
}
if word.startsWith("'") then {
return word.stripPrefix("'")?.stripSuffix("'")?.replaceAll("''", "'") ?! yamlFailure(line, word ++ " isn't a quoted string")
}
match word {
case "" | "~" | "null" | "Null" | "NULL" then { return undefined }
case "true" | "True" | "TRUE" then { return true }
case "false" | "False" | "FALSE" then { return false }
case ".inf" | "+.inf" then { return F64.infinity() }
case "-.inf" then { return -F64.infinity() }
case ".nan" then { return F64.nan() }
case _ then {}
}
if word.startsWith("&") or word.startsWith("*") or word.startsWith("!") then {
error yamlFailure(line, "anchors, aliases and tags aren't read")
}
return word.toInteger() ?? word.stripPrefix("0x")?.toInteger(16) ?? word.toNumber() ?? word
}/// a value on one line - [flow], {flow}, or a scalar
local function inline for Yaml(text: String, line: U64): ParseError ! Unknown {
let word := text.trim()
let parts := λ topLevelParts(word.slice(1..<word.size() - 1)!!, ',').filter(λ(part) part != "")
if word.startsWith("[") and word.endsWith("]") then {
return parts().map(λ(part) inline(part, line)!!).toArray()
}
if word.startsWith("{") and word.endsWith("}") then {
return Map.fromIterable(parts().map(λ(part) {
let (key, value) := splitKey(part) ?! yamlFailure(line, part ++ " isn't key: value")
return (scalar(key, line)!!.toString(), inline(value, line)!!)
}))
}
return scalar(word, line)!!
}/// text split at each separator outside brackets and quotes
local function topLevelParts for Yaml(text: String, separator: Codepoint): Array<String> {
let parts: Array<String> := []
let (depth, start) := (0, 0 as U64)
for c, i in text.codepoints() {
set depth += c == '[' | '{' ? 1 : c == ']' | '}' ? -1 : 0
if c == separator and depth == 0 then {
parts.push(text.slice(start..<i)!!.trim())
set start := i + 1
}
}
parts.push(text.slice(start..<text.size())!!.trim())
return parts
}local method isItem for YamlLine(self: YamlLine): Boolean {
return self.text == "-" or self.text.startsWith("- ")
}/// the value at the line being read - a sequence, a mapping or one scalar
local method block for YamlReader(self: YamlReader): ParseError ! Unknown {
let first := self.lines[self.at] ?! yamlFailure(0, "nothing to read")
if first.isItem() then {
return self.sequence(first.indent)!!
}
if splitKey(first.text) != undefined then {
return self.mapping(first.indent)!!
}
set self.at += 1
return inline(first.text, first.number)!!
}/// what follows a "key:" or "- " - the rest of the line, a block scalar, or a block on the lines below
local method after for YamlReader(self: YamlReader, rest: String, indent: U64, line: U64): ParseError ! Unknown {
if rest.startsWith("|") or rest.startsWith(">") then {
return self.blockScalar(rest, indent, line)
}
if rest != "" then {
return inline(rest, line)!!
}
let next := self.lines[self.at]
if next == undefined or next.indent < indent or (next.indent == indent and not next.isItem()) then {
return undefined
}
return next.indent == indent ? self.sequence(indent)!! : self.block()!!
}local method mapping for YamlReader(self: YamlReader, indent: U64): ParseError ! Map<String, Unknown> {
let map: Map<String, Unknown> := Map.of()
for _ in forever() {
let line := self.lines[self.at]
if line == undefined or line.indent < indent then {
return map
}
if line.indent > indent then {
error yamlFailure(line.number, "a line indented more than the mapping it's in")
}
let (key, rest) := splitKey(line.text) ?? (return map)
set self.at += 1
let name := scalar(key, line.number)!!.toString()
if map.has(name) then {
error yamlFailure(line.number, name ++ " is set twice")
}
map.set(name, self.after(rest, indent, line.number)!!)
}
}local method sequence for YamlReader(self: YamlReader, indent: U64): ParseError ! Array<Unknown> {
let items: Array<Unknown> := []
for _ in forever() {
let line := self.lines[self.at]
if line == undefined or line.indent != indent or not line.isItem() then {
return items
}
let rest := line.text.stripPrefix("-")!!.trimStart()
/// "- key: value" starts a mapping indented as far as key
if splitKey(rest) != undefined and not rest.startsWith(#regex"[\[{\"']") then {
let inner := indent + line.text.size() - rest.size()
set self.lines[self.at] := YamlLine{indent := inner, text := rest, number := line.number}
items.push(self.mapping(inner)!!)
} else {
set self.at += 1
items.push(self.after(rest, indent + 1, line.number)!!)
}
}
}/// a | or > block scalar - the raw lines after line `from` indented past `parent`; > folds single line
/// breaks into spaces; a - after it drops the last line break, a + keeps every trailing one
local method blockScalar for YamlReader(self: YamlReader, header: String, parent: U64, from: U64): String {
let body := self.raw.drop(from).takeWhile(λ(raw) raw.isBlank() or raw.size() - raw.trimStart().size() > parent).toArray()
/// the line reader skips them too
for _ in forever() while self.lines[self.at]?.number?.let(λ(n) n <= from + body.size()) == true {
set self.at += 1
}
let indent := body.filter(λ(raw) not raw.isBlank()).map(λ(raw) raw.size() - raw.trimStart().size()).min() ?? 0
let lines := body.map(λ(raw) raw.isBlank() ? "" : raw.slice(indent..<raw.size())!!).toArray()
let trailing := lines.reverse().takeWhile(λ(line) line == "").count(λ(_) true)
let kept := lines.take(lines.size() - trailing)
let text := header.startsWith(">") ? kept.enumerate().map(λ((i, line)) i == 0 ? line : line == "" or kept[i - 1] == "" or line.startsWith(" ") ? "\n" ++ line : " " ++ line).join("") : kept.join("\n")
return header.contains("-") ? text : header.contains("+") ? text ++ "\n".repeat(trailing + 1) : text ++ "\n"
}#arguments uses #runtime.fieldsOf
global function parse for Arguments<Options: Struct>(args: Array<String>, defaults: Options): ParseError ! Options {
let fields := fieldsOfDefaults(defaults)
let values := flagValues(args, fields)!!
let filled := defaults.clone()
for (field, value) in fields.zip(values) {
set #runtime.fieldsOf(filled)[field.name] := value
}
return filled
}global function help for Arguments<Options: Struct>(defaults: Options, usage: String := ""): String {
return helpText(fieldsOfDefaults(defaults), usage)
} uses #runtime.construct
global function parseFields for Arguments<Options: Struct>(args: Array<String>, fields: Array<ArgumentField>): ParseError ! Options {
let values := flagValues(args, fields)!!
return #runtime.construct(Type.of<Options>(), Map.fromIterable(fields.zip(values).map(λ((field, value)) (field.name, value))))!! as Options
}global function helpFields for Arguments(fields: Array<ArgumentField>, usage: String := ""): String {
return helpText(fields, usage)
}@effects(&stdout, &stderr)
global function forMain for Arguments<Options: Struct>(args: Array<String>, fields: Array<ArgumentField>, usage: String := ""): Options {
let before := args.take(args.indexOf("--") ?? args.size()).toArray()
let hTaken := fields.any(λ(field) shortOf(fields, field) == "h")
if before.contains("--help") or (not hTaken and before.contains("-h")) then {
&stdout.writeText(helpText(fields, usage) ++ "\n")
exit(0)
}
match Arguments.parseFields<Options>(args, fields) {
case Error(e) then {
&stderr.writeText("error: " ++ e.message() ++ "\n\n" ++ helpText(fields, usage) ++ "\n")
exit(2)
}
case options then {
return options
}
}
} uses #runtime.fieldsOf
/// a struct value's fields as flags - each one's default its value in defaults
local function fieldsOfDefaults for Arguments(defaults: Struct): Array<ArgumentField> {
return Type.of(defaults).fields().entries().map(λ((name, type)) ArgumentField{name, type := type.name(), description := "", short := undefined, value := #runtime.fieldsOf(defaults)[name], required := false}).toArray()
}/// rest and passed aren't flags - they take the arguments that aren't, and those after --
local function notAFlag for Arguments(field: ArgumentField): Boolean {
return field.name == "rest" or field.name == "passed"
}/// the letter -x names a field by: its own short ("" for none), or its first letter when it's alone in
/// starting with it
local function shortOf for Arguments(fields: Array<ArgumentField>, field: ArgumentField): String? {
if field.short != undefined then {
return field.short == "" ? undefined : field.short
}
let first := field.name.charAt(0)!!.toString()
let sharing := fields.count(λ(other) not notAFlag(other) and other.name.startsWith(first))
return notAFlag(field) or fields.any(λ(other) other.short == first) or sharing != 1 ? undefined : first
}/// each field's value from args, in fields' order - the flag given, or else the field's default
local function flagValues for Arguments(args: Array<String>, fields: Array<ArgumentField>): ParseError ! Array<Unknown> {
let values: Array<Unknown> := fields.map(λ(field) field.value).toArray()
let given := fields.map(λ(_) false).toArray()
let lists: Map<U64, Array<String>> := Map.of()
let positional: Array<String> := []
let passed: Array<String> := []
let passedAt := fields.findIndex(λ(field) field.name == "passed")
let i: U64 := 0
let optionsOver := false
for _ in forever() while i < args.size() {
let arg := args[i]!!
set i += 1
if optionsOver then {
(passedAt == undefined ? positional : passed).push(arg)
continue _
}
if not arg.startsWith("-") or arg == "-" then {
positional.push(arg)
continue _
}
if arg == "--" then {
set optionsOver := true
continue _
}
/// --name=value, --name value, --flag, --no-flag, -n value
let (body, value) := arg.stripPrefix("--") ?? arg.stripPrefix("-")!!.splitOnce("=")?.let(λ((b, v)) (b, v as String?)) ?? (arg.stripPrefix("--") ?? arg.stripPrefix("-")!!, undefined)
let long := arg.startsWith("--")
let index := long ? fields.findIndex(λ(field) not notAFlag(field) and flagName(field.name) == body) : fields.findIndex(λ(field) shortOf(fields, field) == body)
let negated := long and index == undefined and body.startsWith("no-")
let target := index ?? (negated ? fields.findIndex(λ(field) flagName(field.name) == body.stripPrefix("no-") and field.type == "Boolean") : undefined) ?! ParseError.new("unknown option " ++ arg)
let field := fields[target]!!
let type := field.type.stripSuffix("?") ?? field.type
set given[target] := true
if type == "Boolean" and value == undefined then {
set values[target] := not negated
continue _
}
if negated then {
error ParseError.new(arg ++ " takes no value")
}
let list := type.startsWith("Array<") or type.startsWith("Set<")
/// a flag that can be given alone takes its value only after =
if value == undefined and field.alone != undefined then {
if list then {
lists.getOrInsert(target, λ [])
} else {
set values[target] := field.alone
}
continue _
}
let text := value ?? args[i] ?! ParseError.new(arg ++ " needs a value")
if value == undefined then {
set i += 1
}
if list then {
let items := lists.getOrInsert(target, λ [])
items.pushAll(field.separator == "" ? [text] : text.split(field.separator).map(λ(item) item.trim()).filter(λ(item) item != ""))
} else {
set values[target] := decodedAs(text, type, "--" ++ flagName(field.name))!!
}
}
for (target, texts) in lists.entries() {
set values[target] := decodedAs(texts, fields[target]!!.type, "--" ++ flagName(fields[target]!!.name))!!
}
if not positional.isEmpty() then {
let rest := fields.findIndex(λ(field) field.name == "rest") ?! ParseError.new("unexpected argument " ++ positional[0]!!)
set values[rest] := decodedAs(positional, fields[rest]!!.type, "the arguments")!!
set given[rest] := true
}
if passedAt != undefined and not passed.isEmpty() then {
set values[passedAt] := decodedAs(passed, fields[passedAt]!!.type, "the arguments after --")!!
set given[passedAt] := true
}
for (field, wasGiven) in fields.zip(given) {
if field.required and not wasGiven then {
error ParseError.new(field.name == "rest" ? "arguments are missing" : field.name == "passed" ? "arguments after -- are missing" : "--" ++ flagName(field.name) ++ " is required")
}
}
return values
}/// usage, then a line per flag - its type, then its description, and its default or that it's required.
/// A flag too long to line the rest up with goes on a line of its own, and usage and descriptions are
/// wrapped at 100 columns
local function helpText for Arguments(fields: Array<ArgumentField>, usage: String): String {
let rows := fields.filter(λ(field) not notAFlag(field)).map(λ(field) {
let type := field.type.stripSuffix("?") ?? field.type
let short := shortOf(fields, field)?.let(λ(letter) "-" ++ letter ++ ", ") ?? " "
let list := type.startsWith("Array<") or type.startsWith("Set<")
let shown := list ? "<" ++ Type.named(type).generics()[0]!!.name() ++ ">" ++ field.separator ++ "..." : "<" ++ type ++ ">"
let named := short ++ "--" ++ flagName(field.name)
let flag := type == "Boolean" ? named : field.alone != undefined ? named ++ "[=" ++ shown ++ "]" : named ++ " " ++ shown
let current := field.value
let fact := field.required ? "required" : current == undefined or current == false or current.toString() == "[]" ? "" : "default " ++ current.toString()
let note := field.description == "" ? fact : fact == "" ? field.description : field.description ++ " (" ++ fact ++ ")"
return (flag, note)
}).toArray()
let width := rows.map(λ((flag, _)) flag.size()).filter(λ(size) size <= 28).max() ?? 0
let column := " ".repeat(2 + width + 2)
let lines := usage == "" ? [] : usage.split("\n").flatMap(λ(line) wrapped(line, "", "")).toArray() ++ [""]
lines.push("Options:")
for (flag, note) in rows {
if note == "" then {
lines.push(" " ++ flag)
} else if flag.size() > width then {
lines.push(" " ++ flag)
lines.pushAll(wrapped(note, column, column))
} else {
lines.pushAll(wrapped(note, " " ++ flag.padEnd(width) ++ " ", column))
}
}
return lines.join("\n")
}/// text as lines no wider than 100 columns - the first starting with `first`, the rest with `next`
local function wrapped for Arguments(text: String, first: String, next: String): Array<String> {
let lines: Array<String> := []
let line := first
for word in text.split(" ") {
if line != first and line.size() + 1 + word.size() > 100 then {
lines.push(line)
set line := next ++ word
} else {
set line := line == first ? line ++ word : line ++ " " ++ word
}
}
lines.push(line)
return lines
}/// "maxConnections" as "max-connections" - a "-" before each capital that follows a lowercase letter
local function flagName for Arguments(field: String): String {
return field.replaceAll(#regex"(?<=[a-z0-9])(?=[A-Z])", "-").toLower()
}#argumentfieldLayout
No members of its own.
#maskbits packed 64 to a U64 word, index 0 the lowest bit of the first word; bits past size are always 0
Layout
local method word for Mask(self: Mask, index: U64): U64 {
return self.words[index] ?? 0
}/// clears the bits past width in the last word, after an operation that may have set them
local method trim for Mask(self: Mask): Mask {
let spare := self.width % 64
if spare != 0 and not self.words.isEmpty() then { set self.words[self.words.size() - 1] := self.word(self.words.size() - 1) band ((1 as U64).shl(spare) - 1) }
return self
}local method check for Mask(self: Mask, index: U64): IndexError ! Undefined {
if index >= self.width then { error IndexError.new("bit " ++ index.toString() ++ " is past the end of " ++ self.width.toString() ++ " bits") }
}global function filled for Mask(size: U64, value: Boolean := false): Mask {
return Mask{words := Array.filled((size + 63) / 64, value ? ~(0 as U64) : 0), width := size}.trim()
}global function fromInteger for Mask<Source>(value: Source): Mask {
let mask := Mask.filled(Source.bits() as U64)
for i in 0..<mask.width if value.bit(i as U8) {
{ mask.set(i) !? undefined }
}
return mask
}global method test for Mask(self: Mask, index: U64): IndexError ! Boolean {
self.check(index)!!
return (self.word(index / 64).shr(index % 64) band 1) == 1
}global method set for Mask(self: Mask, index: U64): IndexError ! Undefined {
self.check(index)!!
set self.words[index / 64] := self.word(index / 64) bor (1 as U64).shl(index % 64)
}global method clear for Mask(self: Mask, index: U64): IndexError ! Undefined {
self.check(index)!!
set self.words[index / 64] := self.word(index / 64) band ~(1 as U64).shl(index % 64)
}global method toggle for Mask(self: Mask, index: U64): IndexError ! Undefined {
self.check(index)!!
set self.words[index / 64] := self.word(index / 64) bxor (1 as U64).shl(index % 64)
}global method setAll for Mask(self: Mask): Undefined {
self.words.fill(~(0 as U64))
self.trim()
}global method clearAll for Mask(self: Mask): Undefined {
self.words.fill(0)
}global method count for Mask(self: Mask): U64 {
return self.words.reduce(0 as U64, λ(total, w) total + (w.countOnes() as U64))
}global method any for Mask(self: Mask): Boolean {
return self.words.any(λ(w) w != 0)
}global method all for Mask(self: Mask): Boolean {
return self.count() == self.width
}global method none for Mask(self: Mask): Boolean {
return not self.any()
}global method ones for Mask(self: Mask): Iterator<U64> {
for w, n in self.words {
let rest := w
for _ in forever() while rest != 0 {
yield n * 64 + (rest.trailingZeros() as U64)
set rest := rest band (rest - 1)
}
}
}global method resize for Mask(self: Mask, size: U64): Mask {
let result := Mask.filled(size)
for i in self.ones() while i < size {
result.set(i) !? undefined
}
return result
}global method toInteger for Mask<Target>(self: Mask): CastError ! Target {
let bits := Target.bits() as U64
if self.ones().any(λ(i) i >= bits) then { error CastError.new("a " ++ self.width.toString() ++ "-bit mask with bits set past " ++ bits.toString() ++ " doesn't fit in a " ++ Target.name()) }
let result: Target := 0
for i in self.ones() {
set result := result.withBit(i as U8, true)
}
return result
}#equatable-for-maskmethod equals(self: Mask, other: Mask): Boolean {
return self.width == other.width and self.words == other.words
}#hashable-for-maskmethod hash(self: Mask): U64 {
return self.words.reduce(hashCombine(&hashSeed, self.width), λ(hash, w) hashCombine(hash, w))
}#tostring-for-mask/// highest bit first, like an integer literal - "0b1010"
method toString(self: Mask): String {
return "0b" ++ String.fromCodepoints(Array.generate(self.width, λ(i) (self.test(self.width - 1 - i) !? false) ? '1' : '0'))
}#fromstring-for-mask/// the last digit is bit 0 - the same order an integer literal is written in
function parse(text: String): ParseError ! Mask {
let digits := (text.trim().stripPrefix("0b") ?? text.trim()).replaceAll("_", "").codepoints().reverse()
let mask := Mask.filled(digits.size())
for digit, i in digits {
if digit != '0' and digit != '1' then { error ParseError.new("a mask is written in 0s and 1s, got '" ++ digit.toString() ++ "'") }
if digit == '1' then { mask.set(i) !? undefined }
}
return mask
}#bitwise-for-mask/// a shorter mask counts as 0s past its end, and the result is as wide as the wider one
method bor(self: Mask, other: Mask): Mask {
let n := self.words.size().max(other.words.size())
return Mask{words := Array.generate(n, λ(i) self.word(i) bor other.word(i)), width := self.width.max(other.width)}
}method bxor(self: Mask, other: Mask): Mask {
let n := self.words.size().max(other.words.size())
return Mask{words := Array.generate(n, λ(i) self.word(i) bxor other.word(i)), width := self.width.max(other.width)}
}method band(self: Mask, other: Mask): Mask {
let n := self.words.size().max(other.words.size())
return Mask{words := Array.generate(n, λ(i) self.word(i) band other.word(i)), width := self.width.max(other.width)}
}method bnot(self: Mask): Mask {
return Mask{words := self.words.map(λ(w) ~w), width := self.width}.trim()
}/// toward higher indices - bits shifted past the end are dropped
method shl(self: Mask, count: U64): Mask {
let result := Mask.filled(self.width)
for i in self.ones() if i + count < self.width {
result.set(i + count) !? undefined
}
return result
}method shr(self: Mask, count: U64): Mask {
let result := Mask.filled(self.width)
for i in self.ones() if i >= count {
result.set(i - count) !? undefined
}
return result
}#iterable-boolean-for-maskmethod iterator(self: Mask): Iterator<Boolean> {
for i in 0..<self.width {
yield self.test(i) !? false
}
}#fromiterable-boolean-for-maskfunction fromIterable(items: Iterable<Boolean>): Mask {
let bits := items.toArray()
let mask := Mask.filled(bits.size())
for bit, i in bits if bit {
mask.set(i) !? undefined
}
return mask
}#sized-boolean-for-maskmethod size(self: Mask): U64 {
return self.width
}method isEmpty(self: Mask): Boolean {
return self.width == 0
}#indexable-u64-boolean-for-maskmethod get(self: Mask, key: U64): Boolean? {
return self.test(key) !? undefined
}method set(self: Mask, key: U64, value: Boolean): Error ! Undefined {
return value ? self.set(key) : self.clear(key)
}#default-for-maskfunction default(): Mask {
return Mask.filled(0)
}#clone-for-maskmethod clone(self: Mask): Mask {
return Mask{words := self.words.clone(), width := self.width}
}#regexLayout
/// the pattern (ECMAScript syntax, flags "i", "m", "s", "u", "x") as a backtracking program
local function compile for Regex(pattern: String, flags: String): ParseError ! Bytes {
return #builtin.regexCompile(pattern, flags)
} uses #builtin.regexExecute, #builtin.regexGroups
/// runs the program on text from start - anchored tries only start itself, otherwise each position from
/// start on; positions are code point indexes
local method execute for Regex(self: Regex, text: String, start: U64, anchored: Boolean): Match? {
let spans := #builtin.regexExecute(self.program, text, start, anchored)
return spans == undefined ? undefined : Match{input := text, spans := spans, names := #builtin.regexGroups(self.program)}
}/// "$1", "$name" and "$" in a replacement, filled in from one match
local function expand for Regex(template: String, found: Match): String {
let text := template.codepoints()
let result: Array<Codepoint> := []
let i: U64 := 0
for _ in forever() while i < text.size() {
let c := text[i] ?? ' '
let name := c == '#39; ? text.drop(i + 1).takeWhile(λ(n) n.isIdContinue()) : []
if c == '#39; and text[i + 1] == '#39; then {
result.push('#39;)
set i += 2
} else {
if name.isEmpty() then {
result.push(c)
set i += 1
} else {
let key := String.fromCodepoints(name)
let value := name.all(λ(n) n.isDigit()) ? found.group((key.toInteger() !? 0) as U64) : found.named(key)
set result ++= (value ?? "").codepoints()
set i += 1 + name.size()
}
}
}
return String.fromCodepoints(result)
}global function new for Regex(pattern: String, flags: String := ""): ParseError ! Regex {
return Regex{pattern := pattern, options := flags, program := Self.compile(pattern, flags)!!}
}global function escape for Regex(text: String): String {
return String.fromCodepoints(text.codepoints().flatMap(λ(c) "\\^$.|?*+()[]{}/-".has(c) ? ['\\', c] : [c]))
}global method source for Regex(self: Regex): String {
return self.pattern
}global method flags for Regex(self: Regex): String {
return self.options
}global method test for Regex(self: Regex, text: String): Boolean {
return self.execute(text, 0, false) != undefined
}global method matchAt for Regex(self: Regex, text: String, start: U64): U64 {
return self.execute(text, start, true)?.end() ?? start
}global method find for Regex(self: Regex, text: String, from: U64 := 0): Match? {
return self.execute(text, from, false)
}/// an empty match moves on by one code point, so the search always advances
global method findAll for Regex(self: Regex, text: String): Iterator<Match> {
let from: U64 := 0
let length := text.size()
for _ in forever() while from <= length {
let found := self.execute(text, from, false)
if found == undefined then { return }
yield found
set from := found.end() == found.start() ? found.end() + 1 : found.end()
}
}global method replace for Regex(self: Regex, text: String, with: String | Lambda<Match, String>): String {
let found := self.find(text)
if found == undefined then { return text }
let replacement := with is String ? Self.expand(with as String, found) : (with as Lambda<Match, String>)(found)
return (text.slice(0..<found.start()) !? "") ++ replacement ++ (text.slice(found.end()..<text.size()) !? "")
}global method replaceAll for Regex(self: Regex, text: String, with: String | Lambda<Match, String>): String {
let pieces: Array<String> := []
let last: U64 := 0
for found in self.findAll(text) {
pieces.push(text.slice(last..<found.start()) !? "")
pieces.push(with is String ? Self.expand(with as String, found) : (with as Lambda<Match, String>)(found))
set last := found.end()
}
pieces.push(text.slice(last..<text.size()) !? "")
return pieces.join()
}global method split for Regex(self: Regex, text: String, limit: U64?): Array<String> {
return text.split(self, limit)
} uses #builtin.regexGroups
/// the named groups, in the order they open
global method groupNames for Regex(self: Regex): Array<String> {
return #builtin.regexGroups(self.program).entries().collect().sortedBy(λ(entry) entry[1]).map(λ(entry) entry[0])
}#equatable-for-regexmethod equals(self: Regex, other: Regex): Boolean {
return self.pattern == other.pattern and self.options == other.options
}#hashable-for-regexmethod hash(self: Regex): U64 {
return hashCombine(self.pattern.hash(), self.options.hash())
}#tostring-for-regexmethod toString(self: Regex): String {
return self.pattern
}#clone-for-regexthe program is never changed after compiling, so a clone shares it
method clone(self: Regex): Regex {
return Regex{pattern := self.pattern, options := self.options, program := self.program}
}#query-rowLayout
/// no database injected is an error like any other the database gives
local function database for Query(): IOError ! Database {
return &database ?! IOError.new("no database to run the query on - inject one as &database")
} uses #runtime.construct
/// a row, a Map of its columns, as a Row - a struct's fields decoded from them, a Boolean from the 0
/// or 1 SQLite keeps it as; any other Row is the Map itself
local function row for Query<Row>(self: Query<Row>, columns: Map<String, Unknown>, at: U64): IOError ! Row {
if not Type.of<Row>().isStruct() then {
return columns as Row
}
let values := Type.of<Row>().fields().entries().map(λ((name, type)) {
let value := columns[name]
let read := type.name() == "Boolean" and value is S64 ? value != 0 : value
return (name, decodedAs(read, type.name(), "row " ++ (at + 1).toString() ++ "." ++ name) !? λ(failure) error IOError.new(self.statement.trim() ++ ": " ++ failure.message()))
})
return #runtime.construct(Type.of<Row>(), Map.fromIterable(values))!! as Row
}global method run for Query<Row>(self: Query<Row>): IOError ! Table<Row> {
let rows := Query.database()!!.query(self.statement, self.values)!!
return Table.fromRows(rows.enumerate().map(λ((i, columns)) self.row(columns, i)!!))
}global method first for Query<Row>(self: Query<Row>): IOError ! Row? {
let columns := Query.database()!!.query(self.statement, self.values)!!.first()
return columns == undefined ? undefined : self.row(columns, 0)!!
}global method execute for Query<Row>(self: Query<Row>): IOError ! U64 {
return Query.database()!!.execute(self.statement, self.values)!!
}global method sql for Query<Row>(self: Query<Row>): String {
return self.statement
}global method parameters for Query<Row>(self: Query<Row>): Array<Unknown> {
return self.values.clone()
}#tostring-for-query-rowmethod toString(self: Query<Row>): String {
return self.statement
}#wgslwhat #WGSL builds - its pieces joined, each placeholder already its text
Layout
global method source for Wgsl(self: Wgsl): String {
return self.code
}#equatable-for-wgslmethod equals(self: Wgsl, other: Wgsl): Boolean {
return self.code == other.code
}#tostring-for-wgslmethod toString(self: Wgsl): String {
return self.code
}#glslwhat #GLSL builds - its pieces joined, each placeholder already its text
Layout
global method source for Glsl(self: Glsl): String {
return self.code
}#equatable-for-glslmethod equals(self: Glsl, other: Glsl): Boolean {
return self.code == other.code
}#tostring-for-glslmethod toString(self: Glsl): String {
return self.code
}#hlslwhat #HLSL builds - its pieces joined, each placeholder already its text
Layout
global method source for Hlsl(self: Hlsl): String {
return self.code
}#equatable-for-hlslmethod equals(self: Hlsl, other: Hlsl): Boolean {
return self.code == other.code
}#tostring-for-hlslmethod toString(self: Hlsl): String {
return self.code
}#mslwhat #MSL builds - its pieces joined, each placeholder already its text
Layout
global method source for Msl(self: Msl): String {
return self.code
}#equatable-for-mslmethod equals(self: Msl, other: Msl): Boolean {
return self.code == other.code
}#tostring-for-mslmethod toString(self: Msl): String {
return self.code
}#spirvwhat #SPIRV builds - its pieces joined, each placeholder already its text
Layout
global method source for Spirv(self: Spirv): String {
return self.code
}#equatable-for-spirvmethod equals(self: Spirv, other: Spirv): Boolean {
return self.code == other.code
}#tostring-for-spirvmethod toString(self: Spirv): String {
return self.code
}#commandwhat #SHELL builds - each placeholder already one word of its own, so no shell reads them again
Layout
/// another process - the JavaScript host's (see JavaScript and TypeScript)
@js("host:process", "run")
local function start for Command(words: Array<String>): CommandResultglobal function of for Command(words: Array<String>): Command {
return Command{words := words.clone(), environment := [], piped := []}
}/// a program that can't start comes back from the host as an Error - as an IOError
global method run for Command(self: Command): IOError ! CommandResult {
if self.words.size() == 0 then {
error IOError.new("a command needs a program to run")
}
match Command.start(self.words) {
case Error(e) then {
error IOError.new(e.message())
}
case result then {
return result
}
}
}global method attach for Command(self: Command): IOError ! S64 {
if self.words.size() == 0 then {
error IOError.new("a command needs a program to run")
}
match Command.share(self.words) {
case Error(e) then {
error IOError.new(e.message())
}
case code then {
return code
}
}
}global method words for Command(self: Command): Array<String> {
return self.words.clone()
}/// kept beside the words, and handed to the host with them - "NAME=value" on top of this program's own
global method withEnvironment for Command(self: Command, name: String, value: String): Command {
return Command{words := self.words.clone(), environment := self.environment.filter(λ(pair) not pair.startsWith(name ++ "=")) ++ [name ++ "=" ++ value]}
}global method pipe for Command(self: Command, next: Command): Command {
return Command{words := self.words, environment := self.environment, piped := self.piped ++ [next.words] ++ next.piped}
}global method start for Command(self: Command): IOError ! Process {
if self.words.size() == 0 then {
error IOError.new("a command needs a program to run")
}
let id := Process.spawn([self.words] ++ self.piped, self.environment) !? (error e) IOError.new(e.message())
return Process{id, command := self.toString(), out := [], err := [], exitCode := undefined}
}#tostring-for-commandeach word as the shell would read it back - quoted when it holds a space, a quote or nothing
method toString(self: Command): String {
return self.words.map(λ(word) word.contains(" ") or word.contains("'") or word == "" ? "'" ++ word.replaceAll("'", "'\\''") ++ "'" : word).join(" ")
}#definitionwhat definitionsWith makes in ordinary code - the compiler's findings, as a constant
Layout
global method name for Definition(self: Definition): String {
return self.definitionName
}global method kind for Definition(self: Definition): DefinitionKind {
return self.definitionKind
}global method file for Definition(self: Definition): String {
return self.definitionFile
}global method arguments for Definition(self: Definition): Array<Unknown> {
return self.attributeArguments.clone()
}/// the syntax tree is the compiler's - while compiling, the comptime engine gives it
global method ast for Definition(self: Definition): Error ! Ast.Definition {
error "A definition's syntax tree is only there while compiling - '" ++ self.definitionName ++ "' has none now"
}global method value for Definition(self: Definition): Unknown {
return self.definitionValue
}#tostring-for-definitionmethod toString(self: Definition): String {
return self.definitionKind.toString() ++ " " ++ self.definitionName
}#commandresultLayout
No members of its own.
#processLayout
/// the host's streams - processes, sockets and watchers, each a stream of events (see Process.Event):
/// started, told things, and waited on a little at a time
@js("host:streams", "spawn")
local function spawn for Process(pipeline: Array<Array<String>>, environment: Array<String>): Error ! F64@js("host:streams", "write")
local function writeTo for Process(id: F64, data: String | Bytes): Error ! Undefined@js("host:streams", "closeInput")
local function closeInputOf for Process(id: F64): Error ! Undefined@js("host:streams", "kill")
local function killStream for Process(id: F64): Error ! Undefined@js("host:streams", "next")
local function nextOf for Process(id: F64, ms: F64): Error ! Event?/// stream id's next event, waiting a little at a time - undefined once &cancellation is cancelled
@require(&cancellation)
local function nextEvent for Process(id: F64): IOError ! Event? {
for _ in forever() until &cancellation.isCancelled() {
match Process.nextOf(id, 50.0) {
case Error(e) then {
error IOError.new(e.message())
}
case undefined then {}
case event then {
return event
}
}
}
return undefined
}/// one more event taken in - false once it's exited, or waiting was stopped
@require(&cancellation)
local method pump for Process(self: Process): IOError ! Boolean {
if self.exitCode != undefined then {
return false
}
let event := Process.nextEvent(self.id)!! ?? (return false)
match event.kind {
case "out" then { self.out.pushAll(event.bytes) }
case "err" then { self.err.pushAll(event.bytes) }
case "exit" then {
set self.exitCode := event.code
return false
}
case "error" then {
set self.exitCode := -1
error IOError.new(self.command ++ ": " ++ event.text)
}
case _ then {}
}
return true
}/// buffer's first line, taken out of it, waiting for the process to write one - what's left once it's exited
@require(&cancellation)
local method lineFrom for Process(self: Process, buffer: Array<U8>): IOError ! String? {
for _ in forever() {
let at := buffer.indexOf(10)
if at != undefined then {
let line := Bytes.fromArray(buffer.take(at)).decode<String>() !? ""
buffer.removeWhere(λ(_, i) i <= at)
return line.stripSuffix("\r") ?? line
}
if not self.pump()!! then {
if buffer.isEmpty() then {
return undefined
}
let rest := Bytes.fromArray(buffer.clone()).decode<String>() !? ""
buffer.clear()
return rest
}
}
}global method write for Process(self: Process, data: String | Bytes): IOError ! Undefined {
if self.exitCode != undefined then {
error IOError.new(self.command ++ " has exited - nothing reads what's written to it")
}
Process.writeTo(self.id, data) !? (error e) IOError.new(e.message())
}global method closeInput for Process(self: Process): Undefined {
Process.closeInputOf(self.id)
}global method readLine for Process(self: Process): IOError ! String? {
return self.lineFrom(self.out)!!
}global method readErrorLine for Process(self: Process): IOError ! String? {
return self.lineFrom(self.err)!!
}global method read for Process(self: Process): IOError ! Bytes? {
for _ in forever() while self.out.isEmpty() and self.pump()!! {}
if self.out.isEmpty() then {
return undefined
}
let taken := Bytes.fromArray(self.out.clone())
self.out.clear()
return taken
}global method wait for Process(self: Process): IOError ! S64 {
for _ in forever() while self.pump()!! {}
return self.exitCode ?? -1
}global method kill for Process(self: Process): Undefined {
if self.exitCode == undefined then {
Process.killStream(self.id)
}
}/// what's arrived taken in, without waiting
global method isRunning for Process(self: Process): Boolean {
inject cancellation := Cancellation.new()
for _ in forever() while self.exitCode == undefined {
match Process.nextOf(self.id, 0.0) {
case Error(_) then { return false }
case undefined then { return true }
case event then {
if event.kind == "out" then { self.out.pushAll(event.bytes) }
if event.kind == "err" then { self.err.pushAll(event.bytes) }
if event.kind == "exit" or event.kind == "error" then { set self.exitCode := event.kind == "exit" ? event.code : -1 }
}
}
}
return false
}#tostring-for-processmethod toString(self: Process): String {
return "Process " ++ self.command ++ (self.exitCode == undefined ? " running" : " exited with " ++ self.exitCode.toString())
}#websocketLayout
@js("host:streams", "connect")
local function open for WebSocket(url: String): Error ! F64@js("host:streams", "send")
local function sendTo for WebSocket(id: F64, data: String | Bytes): Error ! Undefined@js("host:streams", "disconnect")
local function closeStream for WebSocket(id: F64, code: S64, reason: String): Error ! Undefinedglobal function connect for WebSocket(url: String | Url): IOError ! WebSocket {
let address := url.toString()
let id := WebSocket.open(address) !? (error e) IOError.new(e.message())
let first := Process.nextEvent(id)!! ?! IOError.new("stopped waiting for " ++ address ++ " to open")
if first.kind != "open" then {
error IOError.new("couldn't connect to " ++ address ++ ": " ++ (first.text == "" ? first.kind : first.text))
}
return WebSocket{id, url := address, open := true}
}global method send for WebSocket(self: WebSocket, message: String | Bytes): IOError ! Undefined {
if not self.open then {
error IOError.new("the socket to " ++ self.url ++ " is closed")
}
WebSocket.sendTo(self.id, message) !? (error e) IOError.new(e.message())
}global method receive for WebSocket(self: WebSocket): IOError ! (String | Bytes)? {
for _ in forever() while self.open {
let event := Process.nextEvent(self.id)!! ?? (return undefined)
match event.kind {
case "message" then {
return event.binary ? event.bytes : event.text
}
case "close" then {
set self.open := false
}
case "error" then {
set self.open := false
error IOError.new("the socket to " ++ self.url ++ " failed: " ++ event.text)
}
case _ then {}
}
}
return undefined
}global method close for WebSocket(self: WebSocket, code: U16 := 1000, reason: String := ""): Undefined {
if self.open then {
WebSocket.closeStream(self.id, code as S64, reason)
set self.open := false
}
}global method isOpen for WebSocket(self: WebSocket): Boolean {
return self.open
}#tostring-for-websocketmethod toString(self: WebSocket): String {
return "WebSocket " ++ self.url ++ (self.open ? "" : " (closed)")
}#filewatcherLayout
@js("host:streams", "watch")
local function watchStream for FileWatcher(path: String, recursive: Boolean): Error ! F64@js("host:streams", "unwatch")
local function unwatchStream for FileWatcher(id: F64): Error ! Undefined/// the host says when it's watching - a change made once new returns is seen
global function new for FileWatcher(path: String | Path, recursive: Boolean := true): IOError ! FileWatcher {
let root := path.toString()
let id := FileWatcher.watchStream(root, recursive) !? (error e) IOError.new(e.message())
inject cancellation := Cancellation.new()
let ready := Process.nextEvent(id)!!
if ready?.kind == "error" then {
error IOError.new("can't watch " ++ root ++ ": " ++ ready!!.text)
}
return FileWatcher{id, root, open := true}
}/// a "change" event as a FileChange
local method changeOf for FileWatcher(self: FileWatcher, event: Process.Event): IOError ! FileChange? {
if event.kind == "error" then {
error IOError.new("watching " ++ self.root ++ " failed: " ++ event.text)
}
if event.kind != "change" then {
return undefined
}
return FileChange{path := event.text, kind := [FileChangeKind.Created, FileChangeKind.Changed, FileChangeKind.Removed][event.code as U64]!!}
}global method next for FileWatcher(self: FileWatcher): IOError ! FileChange? {
for _ in forever() while self.open {
let event := Process.nextEvent(self.id)!! ?? (return undefined)
let change := self.changeOf(event)!!
if change != undefined then {
return change
}
}
return undefined
}/// every distinct change until within has passed
global method changes for FileWatcher(self: FileWatcher, within: Duration): IOError ! Array<FileChange> {
let found: Set<FileChange> := Set.of()
let until := Instant.now() + within
for _ in forever() while self.open and Instant.now() < until {
match Process.nextOf(self.id, (until - Instant.now()).toMilliseconds().min(50.0)) {
case Error(e) then {
error IOError.new(e.message())
}
case undefined then {}
case event then {
self.changeOf(event)!!?.let(λ(change) found.add(change))
}
}
}
return found.toArray()
}global method close for FileWatcher(self: FileWatcher): Undefined {
if self.open then {
FileWatcher.unwatchStream(self.id)
set self.open := false
}
}#tostring-for-filewatchermethod toString(self: FileWatcher): String {
return "FileWatcher " ++ self.root ++ (self.open ? "" : " (closed)")
}#filechangeLayout
No members of its own.
#tostring-for-filechange"Changed src/main.rfn"
method toString(self: FileChange): String {
return self.kind.toString() ++ " " ++ self.path
}#filechangekindNo members of its own.
#equatable-for-filechangekindmethod equals(self: FileChangeKind, other: FileChangeKind): Boolean {
return self.index() == other.index()
}#hashable-for-filechangekindmethod hash(self: FileChangeKind): U64 {
return self.index().hash()
}#tostring-for-filechangekindits name alone - "Changed"
method toString(self: FileChangeKind): String {
return self.variantName()
}#copy-for-filechangekindNothing to write - a marker interface, or one another block already covers.
#matchwhat Regex's execute builds - every position is a code point index into input
Layout
global method text for Match(self: Match): String {
return self.group(0) ?? ""
}global method range for Match(self: Match): Range<U64> {
return self.spans[0] ?? 0..<0
}global method start for Match(self: Match): U64 {
return self.range().start()
}global method end for Match(self: Match): U64 {
return self.range().end()
}global method group for Match(self: Match, index: U64): String? {
let span := self.spans[index]
return span == undefined ? undefined : self.input.slice(span) !? undefined
}global method named for Match(self: Match, name: String): String? {
let index := self.names.get(name)
return index == undefined ? undefined : self.group(index)
}global method groups for Match(self: Match): Array<String?> {
return (1..<self.spans.size()).toArray().map(λ(i) self.group(i))
}global method namedGroups for Match(self: Match): Map<String, String> {
let result: Map<String, String> := Map.of()
for entry in self.names {
let (name, index) := entry
let value := self.group(index)
if value != undefined then { result.set(name, value) }
}
return result
}#equatable-for-matchmethod equals(self: Match, other: Match): Boolean {
return self.input == other.input and self.spans == other.spans
}#tostring-for-matchmethod toString(self: Match): String {
return self.text()
}#clone-for-matchmethod clone(self: Match): Match {
return Match{input := self.input, spans := self.spans.clone(), names := self.names.clone()}
}#requestLayout
No members of its own.
#equatable-for-requestmethod equals(self: Request, other: Request): Boolean {
return self.method == other.method and self.path == other.path and self.params == other.params
and self.query == other.query and self.form == other.form and self.headers == other.headers and self.body == other.body
}#tostring-for-request/// "POST /api/todos"
method toString(self: Request): String {
return self.method ++ " " ++ self.path
}#clone-for-requestmethod clone(self: Request): Request {
return Request{self.method, self.path, self.params.clone(), self.query.clone(), self.form.clone(), self.headers.clone(), self.body}
}#responseLayout
local function typed for Response(body: String | Bytes, contentType: String, status: U16): Response {
return Response{status, headers := Map.of(("Content-Type", contentType)), body}
}global function text for Response(body: String, status: U16 := 200): Response {
return Response.typed(body, "text/plain; charset=utf-8", status)
}global function html for Response(page: Html, status: U16 := 200): Response {
return Response.typed(page.toString(), "text/html; charset=utf-8", status)
}global function json for Response(value: Json, status: U16 := 200): Response {
return Response.typed(value.toString(), "application/json", status)
}global function css for Response(sheet: Stylesheet, status: U16 := 200): Response {
return Response.typed(sheet.toString(), "text/css; charset=utf-8", status)
}global function bytes for Response(body: Bytes, status: U16 := 200): Response {
return Response.typed(body, "application/octet-stream", status)
}global function redirect for Response(to: String, status: U16 := 303): Response {
return Response{status, headers := Map.of(("Location", to))}
}global function empty for Response(status: U16 := 204): Response {
return Response{status}
}global method ok for Response(self: Response): Boolean {
return self.status >= 200 and self.status <= 299
}global method bodyText for Response(self: Response): ParseError ! String {
match self.body {
case String as text then { return text }
case Bytes as bytes then { return String.fromUtf8(bytes)!! }
}
}global method bodyJson for Response(self: Response): ParseError ! Unknown {
let reader := JsonReader{text := self.bodyText()!!.codepoints(), at := 0}
let value := reader.value()!!
reader.space()
if reader.at < reader.text.size() then {
error ParseError.new("not JSON - something after the document")
}
return value
}local method peek for JsonReader(self: JsonReader): Codepoint? {
return self.text[self.at]
}local method space for JsonReader(self: JsonReader): Undefined {
for _ in forever() while self.peek() == ' ' | '\n' | '\r' | '\t' {
set self.at += 1
}
}local method value for JsonReader(self: JsonReader): ParseError ! Unknown {
self.space()
match self.peek() {
case '{' then {
set self.at += 1
let members: Map<String, Unknown> := Map.of()
self.space()
if self.peek() == '}' then { set self.at += 1; return members }
for _ in forever() {
self.space()
let name := self.string()!!
self.space()
self.expect(':')!!
members.set(name, self.value()!!)
self.space()
if self.peek() == '}' then { set self.at += 1; return members }
self.expect(',')!!
}
}
case '[' then {
set self.at += 1
let items: Array<Unknown> := []
self.space()
if self.peek() == ']' then { set self.at += 1; return items }
for _ in forever() {
items.push(self.value()!!)
self.space()
if self.peek() == ']' then { set self.at += 1; return items }
self.expect(',')!!
}
}
case '"' then { return self.string()!! }
case 't' then { self.word("true")!!; return true }
case 'f' then { self.word("false")!!; return false }
case 'n' then { self.word("null")!!; return undefined }
case _ then {
let start := self.at
for _ in forever() while self.peek() in "-+.eE0123456789".codepoints() {
set self.at += 1
}
return String.fromCodepoints(self.text[start..<self.at]).parse<F64>() !? error ParseError.new("not JSON - no value here")
}
}
}local method expect for JsonReader(self: JsonReader, char: Codepoint): ParseError ! Undefined {
if self.peek() != char then {
error ParseError.new("not JSON - expected '" ++ char.toString() ++ "'")
}
set self.at += 1
}local method word for JsonReader(self: JsonReader, word: String): ParseError ! Undefined {
for char in word.codepoints() {
self.expect(char)!!
}
}/// a string - its escapes read, \uXXXX (a surrogate pair one code point) among them
local method string for JsonReader(self: JsonReader): ParseError ! String {
self.expect('"')!!
let out: Array<Codepoint> := []
for _ in forever() {
let char := self.peek() ?? error ParseError.new("not JSON - a string that never ends")
set self.at += 1
if char == '"' then { return String.fromCodepoints(out) }
if char != '\\' then { out.push(char); continue _ }
let escaped := self.peek() ?? ' '
set self.at += 1
match escaped {
case 'u' then {
let code := self.hex()!!
if code >= 0xD800 and code <= 0xDBFF and self.peek() == '\\' then {
set self.at += 2
set code := 0x10000 + (code - 0xD800) * 0x400 + (self.hex()!! - 0xDC00)
}
out.push(Codepoint.of(code)!!)
}
case 'b' then { out.push('\u{8}') }
case 'f' then { out.push('\u{C}') }
case 'n' then { out.push('\n') }
case 'r' then { out.push('\r') }
case 't' then { out.push('\t') }
case '"' | '\\' | '/' then { out.push(escaped) }
case _ then { error ParseError.new("not JSON - '\\" ++ escaped.toString() ++ "' isn't an escape") }
}
}
}local method hex for JsonReader(self: JsonReader): ParseError ! U32 {
let digits := String.fromCodepoints(self.text[self.at..<self.at + 4])
set self.at += 4
return digits.toInteger(16) !? error ParseError.new("not JSON - a \\u escape is four hex digits")
}#equatable-for-responsemethod equals(self: Response, other: Response): Boolean {
return self.status == other.status and self.headers == other.headers and self.body == other.body
}#tostring-for-response/// "404 no todo 7" - its status, and a String body
method toString(self: Response): String {
match self.body {
case String as text then {
return self.status.toString() ++ " " ++ text
}
case _ then {
return self.status.toString()
}
}
}#clone-for-responsemethod clone(self: Response): Response {
return Response{self.status, self.headers.clone(), self.body}
}#httprequestLayout
@require(&http)
global method send for HttpRequest(self: HttpRequest): Promise<IOError ! Response> {
return &http.send(self)
}#equatable-for-httprequestmethod equals(self: HttpRequest, other: HttpRequest): Boolean {
return self.method == other.method and self.url == other.url and self.headers == other.headers and self.body == other.body
and self.followRedirects == other.followRedirects and self.timeout == other.timeout
}#tostring-for-httprequest/// "POST https://api.example.com/todos"
method toString(self: HttpRequest): String {
return self.method ++ " " ++ self.url
}#clone-for-httprequestmethod clone(self: HttpRequest): HttpRequest {
return HttpRequest{self.method, self.url, self.headers.clone(), self.body, self.followRedirects, self.timeout}
}#errorevery error value, and every X: Error type - IndexError.new(...) makes an IndexError. A struct combining Error, `struct Problem { field start: U64 } && Error`, takes these fields - every one but text with its default - and is an error type of its own: Problem{start := 3, text := "..."}
Layout
uses #comptime.callSite
/// where Error.new was called from, when the program was built with locations kept
local function callSite for Error(): String? {
return #comptime.callSite()
}global function new for Error(message: String, cause: Error? := undefined): Error {
return Self{text := message, wrapped := cause, createdAt := Self.callSite(), frames := []}
}global method message for Error(self: Error): String {
return self.text
}global method cause for Error(self: Error): Error? {
return self.wrapped
}global method chain for Error(self: Error): Array<Error> {
let result: Array<Error> := [self]
for _ in forever() while result.last()?.cause() != undefined {
result.push(result.last()?.cause() ?? self)
}
return result
}global method root for Error(self: Error): Error {
return self.chain().last() ?? self
}global method typeOf for Error(self: Error): Type {
return Type.of(self)
}global method is for Error<Target: Error>(self: Error): Boolean {
return self.chain().any(λ(e) Target.isAssignableFrom(e.typeOf()))
}global method find for Error<Target: Error>(self: Error): Target? {
return self.chain().find(λ(e) Target.isAssignableFrom(e.typeOf())) as Target?
}global method wrap for Error(self: Error, message: String): Error {
return Error.new(message, self)
}global method location for Error(self: Error): String? {
return self.createdAt
}global method trace for Error(self: Error): Array<String> {
return self.frames.clone()
}#equatable-for-error/// the same kind of error, saying the same thing, for the same reason
method equals(self: Error, other: Error): Boolean {
return self.typeOf() == other.typeOf() and self.text == other.text and self.wrapped == other.wrapped
}#tostring-for-error/// "IndexError: index 5 is past the end - caused by ..."
method toString(self: Error): String {
return self.chain().map(λ(e) e.typeOf().name() ++ ": " ++ e.message()).join(" - caused by ")
}#clone-for-errormethod clone(self: Error): Error {
return Self{text := self.text, wrapped := self.wrapped?.clone(), createdAt := self.createdAt, frames := self.frames.clone()}
}#optional-typeworks on any Type | Undefined value - self is either a Type or undefined
global method isDefined for Optional<Type>(self: Type?): Boolean {
return self != undefined
}global method isUndefined for Optional<Type>(self: Type?): Boolean {
return self == undefined
}global method orElse for Optional<Type>(self: Type?, fallback: Type): Type {
return self == undefined ? fallback : self
}global method orElseGet for Optional<Type>(self: Type?, fallback: Lambda<(), Type>): Type {
return self == undefined ? fallback() : self
}global method orError for Optional<Type, E: Error>(self: Type?, error: E): E ! Type {
if self == undefined then { error error }
return self
}global method map for Optional<Type, Result>(self: Type?, f: Lambda<Type, Result>): Result? {
return self == undefined ? undefined : f(self)
}global method flatMap for Optional<Type, Result>(self: Type?, f: Lambda<Type, Result?>): Result? {
return self == undefined ? undefined : f(self)
}global method filter for Optional<Type>(self: Type?, keep: Lambda<Type, Boolean>): Type? {
return self != undefined and keep(self) ? self : undefined
}global method ifDefined for Optional<Type>(self: Type?, f: Lambda<Type, Undefined>): Undefined {
if self != undefined then { f(self) }
}global method expect for Optional<Type>(self: Type?, message: String): Type {
return self == undefined ? abort(Error.new(message)) : self
}global method toArray for Optional<Type>(self: Type?): Array<Type> {
return self == undefined ? [] : [self]
}#equatable-for-optional-typeonly when Type is Equatable
method equals(self: Type?, other: Type?): Boolean {
if self == undefined or other == undefined then { return self == undefined and other == undefined }
return (self as Type).equals(other as Type)
}#hashable-for-optional-typeonly when Type is Hashable
method hash(self: Type?): U64 {
return self == undefined ? &hashSeed : hashCombine(&hashSeed + 1, (self as Type).hash())
}#comparable-for-optional-typeonly when Type is Comparable - undefined sorts first
method compare(self: Type?, other: Type?): Ordering {
if self == undefined then { return other == undefined ? Ordering.Equal : Ordering.Less }
if other == undefined then { return Ordering.Greater }
return (self as Type).compare(other as Type)
}#tostring-for-optional-typeonly when Type is ToString
method toString(self: Type?): String {
return self == undefined ? "undefined" : (self as Type).toString()
}#clone-for-optional-typeonly when Type is Clone
method clone(self: Type?): Type? {
return self == undefined ? undefined : (self as Type).clone()
}#copy-for-optional-typeonly when Type is Copy
Nothing to write - a marker interface, or one another block already covers.
#default-for-optional-typefunction default(): Type? {
return undefined
}#iterable-typewhat every Iterable gets - everything is written against iterator() alone, so it works the same on an Array, a Map's (Key, Value) pairs, a Range or a generator's Iterator
global method isEmpty for Iterable<Type>(self: Iterable<Type>): Boolean {
return self.iterator().peek() == undefined
}global method count for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): U64 {
let total: U64 := 0
for item in self if where(item) {
set total += 1
}
return total
}global method contains for Iterable<Type>(self: Iterable<Type>, item: Type): Boolean {
return self.any(λ(x) x == item)
}global method any for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): Boolean {
for item in self if where(item) {
return true
}
return false
}global method all for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): Boolean {
return not self.any(λ(x) not where(x))
}global method none for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): Boolean {
return not self.any(where)
}global method find for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): Type? {
for item in self if where(item) {
return item
}
return undefined
}global method forEach for Iterable<Type>(self: Iterable<Type>, f: Lambda<Type, Undefined>): Undefined {
for item in self {
f(item)
}
}global method reduce for Iterable<Type, Result>(self: Iterable<Type>, initial: Result, f: Lambda<(Result, Type), Result>): Result {
let result := initial
for item in self {
set result := f(result, item)
}
return result
}/// reduce, starting from the first item instead of a given value
global method fold for Iterable<Type>(self: Iterable<Type>, f: Lambda<(Type, Type), Type>): EmptyError ! Type {
let items := self.iterator()
let first := items.next()
if first == undefined then { error EmptyError.new("fold needs at least one item") }
return items.reduce(first, f)
}/// Type: Comparable - the first of several equal ones
global method min for Iterable<Type>(self: Iterable<Type>): EmptyError ! Type {
return self.fold(λ(a, b) b < a ? b : a)
}global method max for Iterable<Type>(self: Iterable<Type>): EmptyError ! Type {
return self.fold(λ(a, b) b > a ? b : a)
}global method minBy for Iterable<Type, Key: Comparable>(self: Iterable<Type>, key: Lambda<Type, Key>): EmptyError ! Type {
return self.fold(λ(a, b) key(b) < key(a) ? b : a)
}global method maxBy for Iterable<Type, Key: Comparable>(self: Iterable<Type>, key: Lambda<Type, Key>): EmptyError ! Type {
return self.fold(λ(a, b) key(b) > key(a) ? b : a)
}/// Type: Numeric
global method sum for Iterable<Type>(self: Iterable<Type>): Type {
return self.reduce(Type.zero(), λ(total, item) total + item)
}global method product for Iterable<Type>(self: Iterable<Type>): Type {
return self.reduce(Type.one(), λ(total, item) total * item)
}global method average for Iterable<Type>(self: Iterable<Type>): EmptyError ! Float {
let (total, n) := self.reduce((0.0, 0 as U64), λ(acc, item) (acc[0] + (item as Float), acc[1] + 1))
if n == 0 then { error EmptyError.new("average needs at least one item") }
return total / (n as Float)
}global method groupBy for Iterable<Type, Key: Hashable>(self: Iterable<Type>, key: Lambda<Type, Key>): Map<Key, Array<Type>> {
let groups: Map<Key, Array<Type>> := Map.of()
for item in self {
groups.getOrInsert(key(item), λ []).push(item)
}
return groups
}global method partition for Iterable<Type>(self: Iterable<Type>, where: Lambda<Type, Boolean>): (Array<Type>, Array<Type>) {
let (kept, rest): (Array<Type>, Array<Type>) := ([], [])
for item in self {
(where(item) ? kept : rest).push(item)
}
return (kept, rest)
}/// Type: ToString
global method join for Iterable<Type>(self: Iterable<Type>, separator: String := ""): String {
return separator.join(self.iterator().map(λ(item) item.toString()))
}global method toArray for Iterable<Type>(self: Iterable<Type>): Array<Type> {
let result: Array<Type> := []
for item in self {
result.push(item)
}
return result
}/// Type: Hashable
global method toSet for Iterable<Type>(self: Iterable<Type>): Set<Type> {
return Set.fromIterable(self)
}global method toMap for Iterable<Type, Key, Value>(self: Iterable<Type>, entry: Lambda<Type, (Key, Value)>): Map<Key, Value> {
return Map.fromIterable(self.iterator().map(entry))
}/// each collection empties itself its own way
global method clear for Iterable<Type>(self: Iterable<Type>): Undefined/// how many times each element occurs, in the order each first did
global method frequencies for Iterable<Type: Hashable>(self: Iterable<Type>): Map<Type, U64> {
let counts: Map<Type, U64> := Map.of()
for x in self {
counts.set(x, counts.getOr(x, 0) + 1)
}
return counts
}/// every element as an F64, sorted - an EmptyError for none
local function sortedFloats for Iterable<Type: Numeric>(items: Iterable<Type>, what: String): EmptyError ! Array<F64> {
let values := items.map(λ(x) x as F64).sorted()
if values.isEmpty() then {
error EmptyError.new(what ++ " of an empty sequence")
}
return values
}/// p percent of the way through the sorted values - between two of them when it falls between
local function percentileOf for Iterable(values: Array<F64>, p: F64): F64 {
let position := p.clamp(0.0..=100.0) / 100.0 * (values.size() - 1) as F64
let below := position.floor()
let low := values[below as U64]!!
let high := values[below as U64 + 1] ?? low
return low + (high - low) * (position - below)
}global method median for Iterable<Type: Numeric>(self: Iterable<Type>): EmptyError ! F64 {
return percentileOf(sortedFloats(self, "median")!!, 50.0)
}global method percentile for Iterable<Type: Numeric>(self: Iterable<Type>, p: F64): EmptyError ! F64 {
return percentileOf(sortedFloats(self, "percentile")!!, p)
}/// of the whole population - divided by the count, not the count less one
global method variance for Iterable<Type: Numeric>(self: Iterable<Type>): EmptyError ! F64 {
let values := self.map(λ(x) x as F64).toArray()
let mean := values.average() !? (error EmptyError.new("variance of an empty sequence"))
return values.map(λ(x) (x - mean) * (x - mean)).sum() / values.size() as F64
}global method standardDeviation for Iterable<Type: Numeric>(self: Iterable<Type>): EmptyError ! F64 {
return self.variance()!!.sqrt()
}#array-typea growable run of Types on &heapAllocator - capacity doubles when it fills up
Layout
local method load for Array<Type>(self: Array<Type>, index: U64): Type {
return self.memory.offset(index as S64).read()
}local method store for Array<Type>(self: Array<Type>, index: U64, value: Type): Undefined {
self.memory.offset(index as S64).write(value)
}local method outOfRange for Array<Type>(self: Array<Type>, index: U64): IndexError {
return IndexError.new("index " ++ index.toString() ++ " is past the end of " ++ self.used.toString() ++ " items")
}/// makes room for at least needed items, moving them to a bigger allocation when there isn't
local method reserve for Array<Type>(self: Array<Type>, needed: U64): Undefined {
if needed <= self.capacity then { return }
let bigger := self.capacity.max(4)
for _ in forever() while bigger < needed {
set bigger *= 2
}
let moved := allocate<Type>(bigger)
for i in 0..<self.used {
moved.offset(i as S64).write(self.load(i))
}
if self.capacity > 0 then { self.memory.free() }
set self.memory := moved
set self.capacity := bigger
}local function empty for Array<Type>(capacity: U64 := 0): Array<Type> {
return Array<Type>{memory := allocate<Type>(capacity), used := 0, capacity := capacity}
}global function filled for Array<Type>(size: U64, fill: Type): Array<Type> {
return Array.generate(size, λ(_) fill)
}global function generate for Array<Type>(size: U64, f: Lambda<U64, Type>): Array<Type> {
let result := Self.empty(size)
for i in 0..<size {
result.push(f(i))
}
return result
}global function of for Array<Type>(items: ..Type): Array<Type> {
return items.toArray()
}global method first for Array<Type>(self: Array<Type>): Type? {
return self.used > 0 ? self.load(0) : undefined
}global method last for Array<Type>(self: Array<Type>): Type? {
return self.used > 0 ? self.load(self.used - 1) : undefined
}global method at for Array<Type>(self: Array<Type>, index: S64): Type? {
let resolved := index < 0 ? (self.used as S64) + index : index
return resolved >= 0 and resolved < self.used as S64 ? self.load(resolved as U64) : undefined
}global method indexOf for Array<Type>(self: Array<Type>, item: Type): U64? {
return self.findIndex(λ(x) x == item)
}global method lastIndexOf for Array<Type>(self: Array<Type>, item: Type): U64? {
for i in (0..<self.used).reverse() if self.load(i) == item {
return i
}
return undefined
}global method findIndex for Array<Type>(self: Array<Type>, where: Lambda<Type, Boolean>): U64? {
for i in 0..<self.used if where(self.load(i)) {
return i
}
return undefined
}global method slice for Array<Type>(self: Array<Type>, range: Range<U64>): IndexError ! Array<Type> {
let (start, end) := bounds(range)
if end > self.used or start > end then { error self.outOfRange(end) }
return Array.generate(end - start, λ(i) self.load(start + i))
}global method push for Array<Type>(self: Array<Type>, item: Type): Undefined {
self.reserve(self.used + 1)
self.store(self.used, item)
set self.used += 1
}global method pop for Array<Type>(self: Array<Type>): Type? {
if self.used == 0 then { return undefined }
set self.used -= 1
return self.load(self.used)
}global method pushFront for Array<Type>(self: Array<Type>, item: Type): Undefined {
self.insert(0, item) !? undefined
}global method popFront for Array<Type>(self: Array<Type>): Type? {
return self.used == 0 ? undefined : self.removeAt(0) !? undefined
}/// shifts everything from index on one place toward the end
global method insert for Array<Type>(self: Array<Type>, index: U64, item: Type): IndexError ! Undefined {
if index > self.used then { error self.outOfRange(index) }
self.reserve(self.used + 1)
for i in (index..<self.used).reverse() {
self.store(i + 1, self.load(i))
}
self.store(index, item)
set self.used += 1
}global method removeAt for Array<Type>(self: Array<Type>, index: U64): IndexError ! Type {
if index >= self.used then { error self.outOfRange(index) }
let removed := self.load(index)
for i in (index + 1)..<self.used {
self.store(i - 1, self.load(i))
}
set self.used -= 1
return removed
}global method remove for Array<Type>(self: Array<Type>, item: Type): Boolean {
let index := self.indexOf(item)
if index == undefined then { return false }
self.removeAt(index) !? undefined
return true
}/// one pass - the kept items slide down over the removed ones
global method removeWhere for Array<Type>(self: Array<Type>, where: Lambda<Type, Boolean>): U64 {
let kept: U64 := 0
for i in 0..<self.used {
let item := self.load(i)
if not where(item) then {
self.store(kept, item)
set kept += 1
}
}
let removed := self.used - kept
set self.used := kept
return removed
}global method pushAll for Array<Type>(self: Array<Type>, items: Iterable<Type>): Undefined {
for item in items {
self.push(item)
}
}global method swap for Array<Type>(self: Array<Type>, i: U64, j: U64): IndexError ! Undefined {
if i >= self.used or j >= self.used then { error self.outOfRange(i.max(j)) }
let held := self.load(i)
self.store(i, self.load(j))
self.store(j, held)
}global method fill for Array<Type>(self: Array<Type>, value: Type): Undefined {
for i in 0..<self.used {
self.store(i, value)
}
}global method resize for Array<Type>(self: Array<Type>, size: U64, fill: Type): Undefined {
if size < self.used then {
self.truncate(size)
} else {
self.reserve(size)
for i in self.used..<size {
self.store(i, fill)
}
set self.used := size
}
}global method truncate for Array<Type>(self: Array<Type>, size: U64): Undefined {
set self.used := self.used.min(size)
}global method clear for Array<Type>(self: Array<Type>): Undefined {
set self.used := 0
}global method map for Array<Type, Result>(self: Array<Type>, f: Lambda<Type, Result>): Array<Result> {
return Array.generate(self.used, λ(i) f(self.load(i)))
}global method mapIndexed for Array<Type, Result>(self: Array<Type>, f: Lambda<(U64, Type), Result>): Array<Result> {
return Array.generate(self.used, λ(i) f(i, self.load(i)))
}global method flatMap for Array<Type, Result>(self: Array<Type>, f: Lambda<Type, Iterable<Result>>): Array<Result> {
let result: Array<Result> := []
for item in self {
set result ++= f(item)
}
return result
}global method filter for Array<Type>(self: Array<Type>, keep: Lambda<Type, Boolean>): Array<Type> {
let result: Array<Type> := []
for item in self if keep(item) {
result.push(item)
}
return result
}global method filterMap for Array<Type, Result>(self: Array<Type>, f: Lambda<Type, Result?>): Array<Result> {
let result: Array<Result> := []
for item in self {
let mapped := f(item)
if mapped != undefined then { result.push(mapped) }
}
return result
}/// Type is Iterable<Inner>
global method flatten for Array<Type, Inner>(self: Array<Type>): Array<Inner> {
return self.flatMap(λ(inner) inner)
}/// as long as the shorter side
global method zip for Array<Type, Other>(self: Array<Type>, other: Iterable<Other>): Array<(Type, Other)> {
return self.iterator().zip(other).collect()
}global method enumerate for Array<Type>(self: Array<Type>): Array<(U64, Type)> {
return self.mapIndexed(λ(i, item) (i, item))
}global method take for Array<Type>(self: Array<Type>, count: U64): Array<Type> {
return self.slice(0..<count.min(self.used)) !? []
}global method drop for Array<Type>(self: Array<Type>, count: U64): Array<Type> {
return self.slice(count.min(self.used)..<self.used) !? []
}global method takeWhile for Array<Type>(self: Array<Type>, where: Lambda<Type, Boolean>): Array<Type> {
return self.take(self.findIndex(λ(x) not where(x)) ?? self.used)
}global method dropWhile for Array<Type>(self: Array<Type>, where: Lambda<Type, Boolean>): Array<Type> {
return self.drop(self.findIndex(λ(x) not where(x)) ?? self.used)
}/// the last chunk is shorter when size doesn't divide the length
global method chunks for Array<Type>(self: Array<Type>, size: U64): Array<Array<Type>> {
let n := size.max(1)
return Array.generate((self.used + n - 1) / n, λ(k) self.slice((k * n)..<((k + 1) * n).min(self.used)) !? [])
}global method windows for Array<Type>(self: Array<Type>, size: U64): Iterator<Window<Type>> {
for start in 0..<(self.used + 1) while size > 0 and start + size <= self.used {
yield Window<Type>{source := self, offset := start, width := size, position := start}
}
}/// Type: Hashable - keeps the first of each
global method distinct for Array<Type>(self: Array<Type>): Array<Type> {
let seen: Set<Type> := Set.of()
return self.filter(λ(item) seen.add(item))
}global method reverse for Array<Type>(self: Array<Type>): Array<Type> {
return Array.generate(self.used, λ(i) self.load(self.used - 1 - i))
}/// positive count moves items toward the front: [1, 2, 3].rotate(1) is [2, 3, 1]
global method rotate for Array<Type>(self: Array<Type>, count: S64): Array<Type> {
if self.used == 0 then { return [] }
let shift := (count.modulo(self.used as S64)) as U64
return Array.generate(self.used, λ(i) self.load((i + shift) % self.used))
}global method sort for Array<Type>(self: Array<Type>): Undefined {
self.sortWith(λ(a, b) a.compare(b))
}global method sortBy for Array<Type, Key: Comparable>(self: Array<Type>, key: Lambda<Type, Key>): Undefined {
self.sortWith(λ(a, b) key(a).compare(key(b)))
}/// merge sort - stable, and n log n whatever the input; runs of width 1, 2, 4, ... merged bottom-up
/// through a scratch array
global method sortWith for Array<Type>(self: Array<Type>, compare: Lambda<(Type, Type), Ordering>): Undefined {
let scratch := self.clone()
let width: U64 := 1
for _ in forever() while width < self.used {
for left in (0..<self.used).step(2 * width) {
let middle := (left + width).min(self.used)
let right := (left + 2 * width).min(self.used)
let (i, j) := (left, middle)
for k in left..<right {
if i < middle and (j >= right or compare(self.load(j), self.load(i)) != Ordering.Less) then {
scratch.store(k, self.load(i))
set i += 1
} else {
scratch.store(k, self.load(j))
set j += 1
}
}
}
for k in 0..<self.used {
self.store(k, scratch.load(k))
}
set width *= 2
}
}global method sorted for Array<Type>(self: Array<Type>): Array<Type> {
let copy := self.clone()
copy.sort()
return copy
}global method sortedBy for Array<Type, Key: Comparable>(self: Array<Type>, key: Lambda<Type, Key>): Array<Type> {
let copy := self.clone()
copy.sortBy(key)
return copy
}global method sortedWith for Array<Type>(self: Array<Type>, compare: Lambda<(Type, Type), Ordering>): Array<Type> {
let copy := self.clone()
copy.sortWith(compare)
return copy
}global method isSorted for Array<Type>(self: Array<Type>): Boolean {
return (1..<self.used.max(1)).all(λ(i) self.load(i - 1) <= self.load(i))
}/// on a sorted array - the error carries where item would be inserted to keep it sorted
global method binarySearch for Array<Type>(self: Array<Type>, item: Type): U64 ! U64 {
let (low, high) := (0 as U64, self.used)
for _ in forever() while low < high {
let middle := low + (high - low) / 2
match self.load(middle).compare(item) {
case Ordering.Less then { set low := middle + 1 }
case Ordering.Greater then { set high := middle }
case _ then { return middle }
}
}
error low
}/// Fisher-Yates - every order equally likely, given a uniform &random
global method shuffle for Array<Type>(self: Array<Type>): Undefined {
for i in (1..<self.used).reverse() {
self.swap(i, &random.between(0..=(i as Integer)) as U64) !? undefined
}
}global method sample for Array<Type>(self: Array<Type>): Type? {
return self.used == 0 ? undefined : self.load(&random.between(0..<(self.used as Integer)) as U64)
}global method cursor for Array<Type>(self: Array<Type>): Cursor<Type> {
return Cursor.over(self)
}global method toStack for Array<Type>(self: Array<Type>): Stack<Type> {
return Stack.fromIterable(self)
}global method toQueue for Array<Type>(self: Array<Type>): Queue<Type> {
return Queue.fromIterable(self)
}global method toDeque for Array<Type>(self: Array<Type>): Deque<Type> {
return Deque.fromIterable(self)
}#iterable-type-for-array-typemethod iterator(self: Array<Type>): Iterator<Type> {
for i in 0..<self.used {
yield self.load(i)
}
}#fromiterable-type-for-array-typefunction fromIterable(items: Iterable<Type>): Array<Type> {
let result := Self.empty()
set result ++= items
return result
}#collection-type-for-array-typemethod contains(self: Array<Type>, item: Type): Boolean {
return self.indexOf(item) != undefined
}#sized-type-for-array-typemethod size(self: Array<Type>): U64 {
return self.used
}method isEmpty(self: Array<Type>): Boolean {
return self.used == 0
}#indexable-u64-type-for-array-typemethod get(self: Array<Type>, key: U64): Type? {
return key < self.used ? self.load(key) : undefined
}method set(self: Array<Type>, key: U64, value: Type): Error ! Undefined {
if key >= self.used then { error self.outOfRange(key) }
self.store(key, value)
}#equatable-for-array-typeonly when Type is Equatable
method equals(self: Array<Type>, other: Array<Type>): Boolean {
return self.used == other.used and (0..<self.used).all(λ(i) self.load(i) == other.load(i))
}#hashable-for-array-typeonly when Type is Hashable
method hash(self: Array<Type>): U64 {
return self.reduce(hashCombine(&hashSeed, self.used), λ(hash, item) hashCombine(hash, item.hash()))
}#comparable-for-array-typeonly when Type is Comparable - the first differing item decides, and a prefix sorts first
method compare(self: Array<Type>, other: Array<Type>): Ordering {
for i in 0..<self.used.min(other.used) {
let order := self.load(i).compare(other.load(i))
if order != Ordering.Equal then { return order }
}
return self.used.compare(other.used)
}#tostring-for-array-type/// "[1, 2, 3]"
method toString(self: Array<Type>): String {
return "[" ++ self.join(", ") ++ "]"
}#concatenable-iterable-type-for-array-typethe right side can be any Iterable - xs ++ someSet, xs ++ 0..<3
/// extend, not ++= - that's this function
method concat(self: Array<Type>, other: Iterable<Type>): Array<Type> {
let result := self.clone()
result.pushAll(other)
return result
}#default-for-array-typefunction default(): Array<Type> {
return Self.empty()
}#clone-for-array-typeshallow - the new array holds the same items
method clone(self: Array<Type>): Array<Type> {
return Array.generate(self.used, λ(i) self.load(i))
}#stack-typean Array whose end is the top
Layout
global method push for Stack<Type>(self: Stack<Type>, item: Type): Undefined {
self.items.push(item)
}global method pop for Stack<Type>(self: Stack<Type>): Type? {
return self.items.pop()
}global method peek for Stack<Type>(self: Stack<Type>): Type? {
return self.items.last()
}global method pushAll for Stack<Type>(self: Stack<Type>, items: Iterable<Type>): Undefined {
set self.items ++= items
}global method drain for Stack<Type>(self: Stack<Type>): Iterator<Type> {
for _ in forever() while not self.items.isEmpty() {
yield self.items.pop() ?? undefined
}
}global method clear for Stack<Type>(self: Stack<Type>): Undefined {
self.items.clear()
}#iterable-type-for-stack-type/// top to bottom - the order pop() would give them
method iterator(self: Stack<Type>): Iterator<Type> {
return self.items.reverse().iterator()
}#fromiterable-type-for-stack-type/// pushed in order, so the last item ends up on top
function fromIterable(items: Iterable<Type>): Stack<Type> {
return Stack<Type>{items := items.toArray()}
}#collection-type-for-stack-typemethod contains(self: Stack<Type>, item: Type): Boolean {
return self.items.contains(item)
}#sized-type-for-stack-typemethod size(self: Stack<Type>): U64 {
return self.items.size()
}method isEmpty(self: Stack<Type>): Boolean {
return self.items.isEmpty()
}#equatable-for-stack-typeonly when Type is Equatable
method equals(self: Stack<Type>, other: Stack<Type>): Boolean {
return self.items == other.items
}#hashable-for-stack-typeonly when Type is Hashable
method hash(self: Stack<Type>): U64 {
return self.items.hash()
}#tostring-for-stack-typemethod toString(self: Stack<Type>): String {
return "Stack" ++ self.items.toString()
}#concatenable-for-stack-typeother's items go on top, in their own order - other's top ends up on top
method concat(self: Stack<Type>, other: Stack<Type>): Stack<Type> {
return Stack<Type>{items := self.items ++ other.items}
}#default-for-stack-typefunction default(): Stack<Type> {
return Stack<Type>{items := []}
}#clone-for-stack-typemethod clone(self: Stack<Type>): Stack<Type> {
return Stack<Type>{items := self.items.clone()}
}#queue-typea Deque used from both ends - in at the back, out at the front - and what onAdd and setClearsAfterHandlers were given
Layout
global method enqueue for Queue<Type>(self: Queue<Type>, item: Type): Undefined {
self.items.pushBack(item)
for handler in self.handlers {
handler(item)
}
if self.clearsAfterHandlers and not self.handlers.isEmpty() then {
self.clear()
}
}global method setClearsAfterHandlers for Queue<Type>(self: Queue<Type>, clears: Boolean): Undefined {
set self.clearsAfterHandlers := clears
}/// in the order they were given - a handler that enqueues runs the handlers again, for that item
global method onAdd for Queue<Type>(self: Queue<Type>, handler: Lambda<Type, Undefined>): Undefined {
self.handlers.push(handler)
}global method dequeue for Queue<Type>(self: Queue<Type>): Type? {
return self.items.popFront()
}global method peek for Queue<Type>(self: Queue<Type>): Type? {
return self.items.peekFront()
}global method enqueueAll for Queue<Type>(self: Queue<Type>, items: Iterable<Type>): Undefined {
for item in items {
self.enqueue(item)
}
}global method drain for Queue<Type>(self: Queue<Type>): Iterator<Type> {
for _ in forever() while not self.items.isEmpty() {
yield self.items.popFront() ?? undefined
}
}global method clear for Queue<Type>(self: Queue<Type>): Undefined {
self.items.clear()
}#iterable-type-for-queue-type/// front to back - the order dequeue() would give them
method iterator(self: Queue<Type>): Iterator<Type> {
return self.items.iterator()
}#fromiterable-type-for-queue-typefunction fromIterable(items: Iterable<Type>): Queue<Type> {
return Queue<Type>{items := Deque.fromIterable(items), handlers := [], clearsAfterHandlers := false}
}#collection-type-for-queue-typemethod contains(self: Queue<Type>, item: Type): Boolean {
return self.items.contains(item)
}#sized-type-for-queue-typemethod size(self: Queue<Type>): U64 {
return self.items.size()
}method isEmpty(self: Queue<Type>): Boolean {
return self.items.isEmpty()
}#equatable-for-queue-typeonly when Type is Equatable
method equals(self: Queue<Type>, other: Queue<Type>): Boolean {
return self.items == other.items
}#hashable-for-queue-typeonly when Type is Hashable
method hash(self: Queue<Type>): U64 {
return self.items.hash()
}#tostring-for-queue-typemethod toString(self: Queue<Type>): String {
return "Queue[" ++ self.join(", ") ++ "]"
}#concatenable-for-queue-typeother's items queue up behind self's - a new queue, with no handlers
method concat(self: Queue<Type>, other: Queue<Type>): Queue<Type> {
return Queue<Type>{items := self.items ++ other.items, handlers := [], clearsAfterHandlers := false}
}#default-for-queue-typefunction default(): Queue<Type> {
return Queue<Type>{items := Deque.default(), handlers := [], clearsAfterHandlers := false}
}#clone-for-queue-typethe items - not the handlers, or setClearsAfterHandlers
method clone(self: Queue<Type>): Queue<Type> {
return Queue<Type>{items := self.items.clone(), handlers := [], clearsAfterHandlers := false}
}#deque-typea ring buffer: the items run from head, wrapping around the end of the allocation - and what onAdd and setClearsAfterHandlers were given
Layout
/// where the index-th item from the front lives
local method slot for Deque<Type>(self: Deque<Type>, index: U64): Pointer<Type> {
return self.memory.offset(((self.head + index) % self.capacity) as S64)
}/// grows by doubling, unwrapping the items to start at 0 in the new allocation
local method reserve for Deque<Type>(self: Deque<Type>, needed: U64): Undefined {
if needed <= self.capacity then { return }
let bigger := self.capacity.max(4)
for _ in forever() while bigger < needed {
set bigger *= 2
}
let moved := allocate<Type>(bigger)
for i in 0..<self.used {
moved.offset(i as S64).write(self.slot(i).read())
}
if self.capacity > 0 then { self.memory.free() }
set self.memory := moved
set self.head := 0
set self.capacity := bigger
}global method pushFront for Deque<Type>(self: Deque<Type>, item: Type): Undefined {
self.reserve(self.used + 1)
set self.head := (self.head + self.capacity - 1) % self.capacity
self.slot(0).write(item)
set self.used += 1
self.added(item)
}global method pushBack for Deque<Type>(self: Deque<Type>, item: Type): Undefined {
self.reserve(self.used + 1)
self.slot(self.used).write(item)
set self.used += 1
self.added(item)
}/// in the order they were given - a handler that pushes runs the handlers again, for that item
global method onAdd for Deque<Type>(self: Deque<Type>, handler: Lambda<Type, Undefined>): Undefined {
self.handlers.push(handler)
}global method setClearsAfterHandlers for Deque<Type>(self: Deque<Type>, clears: Boolean): Undefined {
set self.clearsAfterHandlers := clears
}local method added for Deque<Type>(self: Deque<Type>, item: Type): Undefined {
for handler in self.handlers {
handler(item)
}
if self.clearsAfterHandlers and not self.handlers.isEmpty() then {
self.clear()
}
}global method popFront for Deque<Type>(self: Deque<Type>): Type? {
if self.used == 0 then { return undefined }
let item := self.slot(0).read()
set self.head := (self.head + 1) % self.capacity
set self.used -= 1
return item
}global method popBack for Deque<Type>(self: Deque<Type>): Type? {
if self.used == 0 then { return undefined }
set self.used -= 1
return self.slot(self.used).read()
}global method peekFront for Deque<Type>(self: Deque<Type>): Type? {
return self.used == 0 ? undefined : self.slot(0).read()
}global method peekBack for Deque<Type>(self: Deque<Type>): Type? {
return self.used == 0 ? undefined : self.slot(self.used - 1).read()
}/// positive count moves items from the front to the back, like Array.rotate - nothing is added, so no handler runs
global method rotate for Deque<Type>(self: Deque<Type>, count: S64): Undefined {
if self.used == 0 then { return }
for _ in 0..<(count.modulo(self.used as S64) as U64) {
let item := self.popFront() ?? undefined
self.slot(self.used).write(item)
set self.used += 1
}
}global method toArray for Deque<Type>(self: Deque<Type>): Array<Type> {
return Array.generate(self.used, λ(i) self.slot(i).read())
}global method clear for Deque<Type>(self: Deque<Type>): Undefined {
set self.head := 0
set self.used := 0
}#iterable-type-for-deque-typemethod iterator(self: Deque<Type>): Iterator<Type> {
for i in 0..<self.used {
yield self.slot(i).read()
}
}#fromiterable-type-for-deque-typefunction fromIterable(items: Iterable<Type>): Deque<Type> {
let result := Deque<Type>.default()
for item in items {
result.pushBack(item)
}
return result
}#collection-type-for-deque-typemethod contains(self: Deque<Type>, item: Type): Boolean {
return self.iterator().any(λ(x) x == item)
}#sized-type-for-deque-typemethod size(self: Deque<Type>): U64 {
return self.used
}method isEmpty(self: Deque<Type>): Boolean {
return self.used == 0
}#indexable-u64-type-for-deque-typemethod get(self: Deque<Type>, key: U64): Type? {
return key < self.used ? self.slot(key).read() : undefined
}method set(self: Deque<Type>, key: U64, value: Type): Error ! Undefined {
if key >= self.used then { error IndexError.new("index " ++ key.toString() ++ " is past the end of " ++ self.used.toString() ++ " items") }
self.slot(key).write(value)
}#equatable-for-deque-typeonly when Type is Equatable
method equals(self: Deque<Type>, other: Deque<Type>): Boolean {
return self.toArray() == other.toArray()
}#hashable-for-deque-typeonly when Type is Hashable
method hash(self: Deque<Type>): U64 {
return self.toArray().hash()
}#tostring-for-deque-typemethod toString(self: Deque<Type>): String {
return "Deque" ++ self.toArray().toString()
}#concatenable-for-deque-typea new deque, with no handlers
method concat(self: Deque<Type>, other: Deque<Type>): Deque<Type> {
let result := self.clone()
for item in other {
result.pushBack(item)
}
return result
}#default-for-deque-typefunction default(): Deque<Type> {
return Deque<Type>{memory := allocate<Type>(0), head := 0, used := 0, capacity := 0, handlers := [], clearsAfterHandlers := false}
}#clone-for-deque-typethe items - not the handlers, or setClearsAfterHandlers
method clone(self: Deque<Type>): Deque<Type> {
return Deque.fromIterable(self)
}#set-typea Map from each item to true - Type: Hashable
Layout
global function of for Set<Type>(items: ..Type): Set<Type> {
return Set.fromIterable(items)
}global method add for Set<Type>(self: Set<Type>, item: Type): Boolean {
if self.members.has(item) then { return false }
self.members.set(item, true)
return true
}global method remove for Set<Type>(self: Set<Type>, item: Type): Boolean {
return self.members.remove(item) != undefined
}global method addAll for Set<Type>(self: Set<Type>, items: Iterable<Type>): Undefined {
for item in items {
self.add(item)
}
}global method removeAll for Set<Type>(self: Set<Type>, items: Iterable<Type>): Undefined {
for item in items {
self.remove(item)
}
}global method retain for Set<Type>(self: Set<Type>, where: Lambda<Type, Boolean>): Undefined {
for item in self.toArray() if not where(item) {
self.remove(item)
}
}global method intersection for Set<Type>(self: Set<Type>, other: Set<Type>): Set<Type> {
return self.filter(λ(item) other.has(item))
}global method difference for Set<Type>(self: Set<Type>, other: Set<Type>): Set<Type> {
return self.filter(λ(item) not other.has(item))
}global method symmetricDifference for Set<Type>(self: Set<Type>, other: Set<Type>): Set<Type> {
return self.difference(other) ++ other.difference(self)
}global method isSubsetOf for Set<Type>(self: Set<Type>, other: Set<Type>): Boolean {
return self.all(λ(item) other.has(item))
}global method isSupersetOf for Set<Type>(self: Set<Type>, other: Set<Type>): Boolean {
return other.isSubsetOf(self)
}global method isDisjointFrom for Set<Type>(self: Set<Type>, other: Set<Type>): Boolean {
return self.none(λ(item) other.has(item))
}global method map for Set<Type, Result: Hashable>(self: Set<Type>, f: Lambda<Type, Result>): Set<Result> {
return Set.fromIterable(self.iterator().map(f))
}global method filter for Set<Type>(self: Set<Type>, keep: Lambda<Type, Boolean>): Set<Type> {
return Set.fromIterable(self.iterator().filter(keep))
}global method clear for Set<Type>(self: Set<Type>): Undefined {
self.members.clear()
}#iterable-type-for-set-type/// in the order the items were first added
method iterator(self: Set<Type>): Iterator<Type> {
return self.members.keys()
}#fromiterable-type-for-set-typefunction fromIterable(items: Iterable<Type>): Set<Type> {
let result := Set<Type>{members := Map.of()}
result.addAll(items)
return result
}#collection-type-for-set-typemethod contains(self: Set<Type>, item: Type): Boolean {
return self.members.has(item)
}#sized-type-for-set-typemethod size(self: Set<Type>): U64 {
return self.members.size()
}method isEmpty(self: Set<Type>): Boolean {
return self.members.isEmpty()
}#equatable-for-set-typethe same items, whatever order they were added in
method equals(self: Set<Type>, other: Set<Type>): Boolean {
return self.size() == other.size() and self.isSubsetOf(other)
}#hashable-for-set-typeorder-independent, like equality - the items' hashes are summed
method hash(self: Set<Type>): U64 {
return self.reduce(&hashSeed, λ(total, item) total.wrappingAdd(item.hash()))
}#tostring-for-set-type/// "Set{1, 2, 3}"
method toString(self: Set<Type>): String {
return "Set{" ++ self.join(", ") ++ "}"
}#concatenable-for-set-typethe union - every item in either
method concat(self: Set<Type>, other: Set<Type>): Set<Type> {
let result := self.clone()
result.addAll(other)
return result
}#default-for-set-typefunction default(): Set<Type> {
return Set<Type>{members := Map.of()}
}#clone-for-set-typemethod clone(self: Set<Type>): Set<Type> {
return Set<Type>{members := self.members.clone()}
}#map-key-valuea hash table that remembers insertion order: pairs holds the entries in the order they were added (a removed one leaves an undefined hole), and table maps a key's hash to its place in pairs - open addressing, probing linearly, kept at most two thirds full
Layout
/// marks a table slot whose entry was removed - probing carries on past it, unlike an empty one
local const removedSlot: U64 := U64.max()/// the table slot and pairs index of key, when it's in the map
local method locate for Map<Key, Value>(self: Map<Key, Value>, key: Key): (U64, U64)? {
if self.table.isEmpty() then { return undefined }
let mask := self.table.size() - 1
let slot := key.hash() band mask
for _ in forever() {
let index := self.table[slot] ?? undefined
if index == undefined then { return undefined }
if index != removedSlot and self.pairs[index]?[0] == key then { return (slot, index) }
set slot := (slot + 1) band mask
}
}/// a fresh table - a power of two, at least 8, big enough for atLeast entries at two thirds full -
/// with the holes squeezed out of pairs
local method rebuild for Map<Key, Value>(self: Map<Key, Value>, atLeast: U64): Undefined {
let size: U64 := 8
for _ in forever() while size * 2 < atLeast * 3 {
set size *= 2
}
set self.pairs := self.pairs.filter(λ(entry) entry != undefined)
set self.table := Array.filled(size, undefined)
for entry, index in self.pairs {
let slot := (entry?[0]?.hash() ?? 0) band (size - 1)
for _ in forever() while self.table[slot] != undefined {
set slot := (slot + 1) band (size - 1)
}
set self.table[slot] := index
}
}global function of for Map<Key, Value>(entries: ..(Key, Value)): Map<Key, Value> {
return Map.fromIterable(entries)
}global function fromKeys for Map<Key, Value>(keys: Iterable<Key>, value: Lambda<Key, Value>): Map<Key, Value> {
return Map.fromIterable(keys.iterator().map(λ(key) (key, value(key))))
}global method has for Map<Key, Value>(self: Map<Key, Value>, key: Key): Boolean {
return self.locate(key) != undefined
}global method getOr for Map<Key, Value>(self: Map<Key, Value>, key: Key, fallback: Value): Value {
return self.has(key) ? self.get(key) as Value : fallback
}global method getOrInsert for Map<Key, Value>(self: Map<Key, Value>, key: Key, value: Lambda<(), Value>): Value {
if not self.has(key) then { self.set(key, value()) }
return self.get(key) as Value
}global method remove for Map<Key, Value>(self: Map<Key, Value>, key: Key): Value? {
let found := self.locate(key)
if found == undefined then { return undefined }
let (slot, index) := found
let value := self.pairs[index]?[1]
set self.pairs[index] := undefined
set self.table[slot] := removedSlot
set self.live -= 1
return value
}global method update for Map<Key, Value>(self: Map<Key, Value>, key: Key, f: Lambda<Value?, Value?>): Undefined {
let next := f(self.get(key))
if next == undefined then {
self.remove(key)
} else {
self.set(key, next)
}
}global method merge for Map<Key, Value>(self: Map<Key, Value>, other: Map<Key, Value>, combine: Lambda<(Value, Value), Value>?): Undefined {
for entry in other {
let (key, value) := entry
self.set(key, combine != undefined and self.has(key) ? combine(self.get(key) as Value, value) : value)
}
}global method keys for Map<Key, Value>(self: Map<Key, Value>): Iterator<Key> {
return self.entries().map(λ(entry) entry[0])
}global method values for Map<Key, Value>(self: Map<Key, Value>): Iterator<Value> {
return self.entries().map(λ(entry) entry[1])
}global method entries for Map<Key, Value>(self: Map<Key, Value>): Iterator<(Key, Value)> {
for entry in self.pairs if entry != undefined {
yield entry
}
}global method mapValues for Map<Key, Value, Result>(self: Map<Key, Value>, f: Lambda<Value, Result>): Map<Key, Result> {
return Map.fromIterable(self.entries().map(λ(entry) (entry[0], f(entry[1]))))
}global method filter for Map<Key, Value>(self: Map<Key, Value>, keep: Lambda<(Key, Value), Boolean>): Map<Key, Value> {
return Map.fromIterable(self.entries().filter(keep))
}/// Value: Hashable - when two keys share a value, the later one wins
global method invert for Map<Key, Value>(self: Map<Key, Value>): Map<Value, Key> {
return Map.fromIterable(self.entries().map(λ(entry) (entry[1], entry[0])))
}global method clear for Map<Key, Value>(self: Map<Key, Value>): Undefined {
set self.pairs := []
set self.table := []
set self.live := 0
}#iterable-key-value-for-map-key-valuemethod iterator(self: Map<Key, Value>): Iterator<(Key, Value)> {
return self.entries()
}#fromiterable-key-value-for-map-key-value/// a key seen twice keeps its last value, and its first position
function fromIterable(items: Iterable<(Key, Value)>): Map<Key, Value> {
let result := Map<Key, Value>{pairs := [], table := [], live := 0}
for entry in items {
let (key, value) := entry
result.set(key, value)
}
return result
}#collection-key-value-for-map-key-valuemethod contains(self: Map<Key, Value>, item: (Key, Value)): Boolean {
return self.has(item[0]) and self.get(item[0]) == item[1]
}#sized-key-value-for-map-key-valuemethod size(self: Map<Key, Value>): U64 {
return self.live
}method isEmpty(self: Map<Key, Value>): Boolean {
return self.live == 0
}#indexable-key-value-for-map-key-valuemethod get(self: Map<Key, Value>, key: Key): Value? {
let found := self.locate(key)
return found == undefined ? undefined : self.pairs[found[1]]?[1]
}/// a new key goes into the first empty or removed slot along its probe
method set(self: Map<Key, Value>, key: Key, value: Value): Undefined {
let found := self.locate(key)
if found != undefined then {
set self.pairs[found[1]] := (key, value)
return
}
if (self.pairs.size() + 1) * 3 > self.table.size() * 2 then { self.rebuild((self.live + 1) * 2) }
let mask := self.table.size() - 1
let slot := key.hash() band mask
for _ in forever() while self.table[slot] != undefined and self.table[slot] != removedSlot {
set slot := (slot + 1) band mask
}
set self.table[slot] := self.pairs.size()
self.pairs.push((key, value))
set self.live += 1
}#equatable-for-map-key-valuethe same pairs, whatever order they were added in
method equals(self: Map<Key, Value>, other: Map<Key, Value>): Boolean {
return self.live == other.live and self.entries().all(λ(entry) other.has(entry[0]) and other.get(entry[0]) == entry[1])
}#hashable-for-map-key-valueonly when Value is Hashable - order-independent, like equality
method hash(self: Map<Key, Value>): U64 {
return self.entries().reduce(&hashSeed, λ(total, entry) total.wrappingAdd(hashCombine(entry[0].hash(), entry[1].hash())))
}#tostring-for-map-key-value/// "{a: 1, b: 2}"
method toString(self: Map<Key, Value>): String {
return "{" ++ self.entries().map(λ(entry) entry[0].toString() ++ ": " ++ entry[1].toString()).join(", ") ++ "}"
}#concatenable-for-map-key-valueevery entry of both - where both have a key, other's value wins (merge takes a combine for that)
method concat(self: Map<Key, Value>, other: Map<Key, Value>): Map<Key, Value> {
let result := self.clone()
result.merge(other, undefined)
return result
}#default-for-map-key-valuefunction default(): Map<Key, Value> {
return Map.of()
}#clone-for-map-key-valueshallow - the new map holds the same keys and values
method clone(self: Map<Key, Value>): Map<Key, Value> {
return Map.fromIterable(self.entries())
}#bimap-a-btwo Maps kept in step - one each way
Layout
global method getByLeft for BiMap<A, B>(self: BiMap<A, B>, a: A): B? {
return self.forward.get(a)
}global method getByRight for BiMap<A, B>(self: BiMap<A, B>, b: B): A? {
return self.backward.get(b)
}global method hasLeft for BiMap<A, B>(self: BiMap<A, B>, a: A): Boolean {
return self.forward.has(a)
}global method hasRight for BiMap<A, B>(self: BiMap<A, B>, b: B): Boolean {
return self.backward.has(b)
}global method removeByLeft for BiMap<A, B>(self: BiMap<A, B>, a: A): B? {
let b := self.forward.remove(a)
if b != undefined then { self.backward.remove(b) }
return b
}global method removeByRight for BiMap<A, B>(self: BiMap<A, B>, b: B): A? {
let a := self.backward.remove(b)
if a != undefined then { self.forward.remove(a) }
return a
}global method lefts for BiMap<A, B>(self: BiMap<A, B>): Iterator<A> {
return self.forward.keys()
}global method rights for BiMap<A, B>(self: BiMap<A, B>): Iterator<B> {
return self.forward.values()
}/// the same two Maps, swapped - changing either BiMap changes both
global method inverse for BiMap<A, B>(self: BiMap<A, B>): BiMap<B, A> {
return BiMap<B, A>{forward := self.backward, backward := self.forward}
}global method toMap for BiMap<A, B>(self: BiMap<A, B>): Map<A, B> {
return self.forward.clone()
}global method clear for BiMap<A, B>(self: BiMap<A, B>): Undefined {
self.forward.clear()
self.backward.clear()
}#iterable-a-b-for-bimap-a-bmethod iterator(self: BiMap<A, B>): Iterator<(A, B)> {
return self.forward.entries()
}#fromiterable-a-b-for-bimap-a-bfunction fromIterable(items: Iterable<(A, B)>): BiMap<A, B> {
let result := BiMap<A, B>{forward := Map.of(), backward := Map.of()}
for pair in items {
let (a, b) := pair
result.set(a, b)
}
return result
}#collection-a-b-for-bimap-a-bmethod contains(self: BiMap<A, B>, item: (A, B)): Boolean {
return self.forward.contains(item)
}#sized-a-b-for-bimap-a-bmethod size(self: BiMap<A, B>): U64 {
return self.forward.size()
}method isEmpty(self: BiMap<A, B>): Boolean {
return self.forward.isEmpty()
}#indexable-a-b-for-bimap-a-bmethod set(self: BiMap<A, B>, a: A, b: B): Undefined {
self.removeByLeft(a)
self.removeByRight(b)
self.forward.set(a, b)
self.backward.set(b, a)
}method get(self: BiMap<A, B>, key: A): B? {
return self.getByLeft(key)
}#equatable-for-bimap-a-bmethod equals(self: BiMap<A, B>, other: BiMap<A, B>): Boolean {
return self.forward == other.forward
}#hashable-for-bimap-a-bmethod hash(self: BiMap<A, B>): U64 {
return self.forward.hash()
}#tostring-for-bimap-a-bmethod toString(self: BiMap<A, B>): String {
return "BiMap" ++ self.forward.toString()
}#default-for-bimap-a-bfunction default(): BiMap<A, B> {
return BiMap<A, B>{forward := Map.of(), backward := Map.of()}
}#clone-for-bimap-a-bmethod clone(self: BiMap<A, B>): BiMap<A, B> {
return BiMap<A, B>{forward := self.forward.clone(), backward := self.backward.clone()}
}#priorityqueue-typeLayout
global function new for PriorityQueue<Type: Comparable>(): PriorityQueue<Type> {
return PriorityQueue<Type>{heap := [], first := λ(a, b) a < b}
}global function withOrder for PriorityQueue<Type>(compare: Lambda<(Type, Type), Ordering>): PriorityQueue<Type> {
return PriorityQueue<Type>{heap := [], first := λ(a, b) compare(a, b) == Ordering.Less}
}/// the item at `at` moved up until its parent comes first
local method up for PriorityQueue<Type>(self: PriorityQueue<Type>, at: U64): Undefined {
for _ in forever() while at > 0 and self.first(self.heap[at]!!, self.heap[(at - 1) // 2]!!) {
self.heap.swap(at, (at - 1) // 2)
set at := (at - 1) // 2
}
}/// the item at `at` moved down until both its children come after it
local method down for PriorityQueue<Type>(self: PriorityQueue<Type>, at: U64): Undefined {
for _ in forever() {
let best := [at * 2 + 1, at * 2 + 2].filter(λ(child) child < self.heap.size()).reduce(at, λ(best, child) self.first(self.heap[child]!!, self.heap[best]!!) ? child : best)
if best == at then {
return undefined
}
self.heap.swap(at, best)
set at := best
}
}global method push for PriorityQueue<Type>(self: PriorityQueue<Type>, item: Type): Undefined {
self.heap.push(item)
self.up(self.heap.size() - 1)
}global method pop for PriorityQueue<Type>(self: PriorityQueue<Type>): Type? {
if self.heap.isEmpty() then {
return undefined
}
self.heap.swap(0, self.heap.size() - 1)
let top := self.heap.pop()
self.down(0)
return top
}global method peek for PriorityQueue<Type>(self: PriorityQueue<Type>): Type? {
return self.heap[0]
}global method pushAll for PriorityQueue<Type>(self: PriorityQueue<Type>, items: Iterable<Type>): Undefined {
for item in items {
self.push(item)
}
}global method drain for PriorityQueue<Type>(self: PriorityQueue<Type>): Iterator<Type> {
for _ in forever() while not self.heap.isEmpty() {
yield self.pop() ?? undefined
}
}global method toArray for PriorityQueue<Type>(self: PriorityQueue<Type>): Array<Type> {
return self.clone().drain().collect()
}global method clear for PriorityQueue<Type>(self: PriorityQueue<Type>): Undefined {
self.heap.clear()
}#iterable-type-for-priorityqueue-typein the heap's own order - not the order pop gives
method iterator(self: PriorityQueue<Type>): Iterator<Type> {
return self.heap.iterator()
}#fromiterable-type-for-priorityqueue-typesmallest first - Type: Comparable
function fromIterable(items: Iterable<Type>): PriorityQueue<Type> {
let queue := PriorityQueue<Type>.new()
queue.pushAll(items)
return queue
}#collection-type-for-priorityqueue-typemethod contains(self: PriorityQueue<Type>, item: Type): Boolean {
return self.heap.contains(item)
}#sized-type-for-priorityqueue-typemethod size(self: PriorityQueue<Type>): U64 {
return self.heap.size()
}method isEmpty(self: PriorityQueue<Type>): Boolean {
return self.heap.isEmpty()
}#equatable-for-priorityqueue-typethe same items in the same order
method equals(self: PriorityQueue<Type>, other: PriorityQueue<Type>): Boolean {
return self.toArray() == other.toArray()
}#hashable-for-priorityqueue-typemethod hash(self: PriorityQueue<Type>): U64 {
return self.toArray().hash()
}#tostring-for-priorityqueue-typemethod toString(self: PriorityQueue<Type>): String {
return "PriorityQueue" ++ self.toArray().toString()
}#default-for-priorityqueue-typefunction default(): PriorityQueue<Type> {
return PriorityQueue<Type>.new()
}#clone-for-priorityqueue-typemethod clone(self: PriorityQueue<Type>): PriorityQueue<Type> {
return PriorityQueue<Type>{heap := self.heap.clone(), first := self.first}
}#sortedmap-key-valueLayout
global function new for SortedMap<Key: Comparable, Value>(): SortedMap<Key, Value> {
return SortedMap<Key, Value>{keyList := [], valueList := []}
}global function of for SortedMap<Key: Comparable, Value>(entries: ..(Key, Value)): SortedMap<Key, Value> {
return SortedMap<Key, Value>.fromIterable(entries)
}/// where key is, or would go - binary search over the keys
local method slot for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): U64 {
return self.keyList.binarySearch(key) ?? (error e) e
}local method entryAt for SortedMap<Key, Value>(self: SortedMap<Key, Value>, at: U64): (Key, Value)? {
return at < self.keyList.size() ? (self.keyList[at]!!, self.valueList[at]!!) : undefined
}global method get for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): Value? {
let at := self.slot(key)
return self.keyList[at] == key ? self.valueList[at] : undefined
}global method set for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key, value: Value): Undefined {
let at := self.slot(key)
if self.keyList[at] == key then {
set self.valueList[at] := value
} else {
self.keyList.insert(at, key)!!
self.valueList.insert(at, value)!!
}
}global method has for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): Boolean {
return self.keyList[self.slot(key)] == key
}global method getOr for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key, fallback: Value): Value {
return self.has(key) ? self.get(key)!! : fallback
}global method remove for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): Value? {
let at := self.slot(key)
if self.keyList[at] != key then {
return undefined
}
self.keyList.removeAt(at)!!
return self.valueList.removeAt(at)!!
}global method first for SortedMap<Key, Value>(self: SortedMap<Key, Value>): (Key, Value)? {
return self.entryAt(0)
}global method last for SortedMap<Key, Value>(self: SortedMap<Key, Value>): (Key, Value)? {
return self.keyList.isEmpty() ? undefined : self.entryAt(self.keyList.size() - 1)
}global method atOrBelow for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): (Key, Value)? {
let at := self.slot(key)
return self.keyList[at] == key ? self.entryAt(at) : at == 0 ? undefined : self.entryAt(at - 1)
}global method atOrAbove for SortedMap<Key, Value>(self: SortedMap<Key, Value>, key: Key): (Key, Value)? {
return self.entryAt(self.slot(key))
}global method between for SortedMap<Key, Value>(self: SortedMap<Key, Value>, range: Range<Key>): Iterator<(Key, Value)> {
for at in self.slot(range.start())..<self.keyList.size() while range.contains(self.keyList[at]!!) or self.keyList[at]!! < range.start() {
yield self.entryAt(at)!!
}
}global method keys for SortedMap<Key, Value>(self: SortedMap<Key, Value>): Iterator<Key> {
return self.keyList.iterator()
}global method values for SortedMap<Key, Value>(self: SortedMap<Key, Value>): Iterator<Value> {
return self.valueList.iterator()
}global method entries for SortedMap<Key, Value>(self: SortedMap<Key, Value>): Iterator<(Key, Value)> {
return self.keyList.iterator().zip(self.valueList)
}global method toMap for SortedMap<Key, Value>(self: SortedMap<Key, Value>): Map<Key, Value> {
return Map.fromIterable(self.entries())
}global method clear for SortedMap<Key, Value>(self: SortedMap<Key, Value>): Undefined {
self.keyList.clear()
self.valueList.clear()
}#iterable-key-value-for-sortedmap-key-valuemethod iterator(self: SortedMap<Key, Value>): Iterator<(Key, Value)> {
return self.entries()
}#fromiterable-key-value-for-sortedmap-key-valuefunction fromIterable(entries: Iterable<(Key, Value)>): SortedMap<Key, Value> {
let map := SortedMap<Key, Value>.new()
for (key, value) in entries {
map.set(key, value)
}
return map
}#collection-key-value-for-sortedmap-key-valuemethod contains(self: SortedMap<Key, Value>, entry: (Key, Value)): Boolean {
return self.has(entry.0) and self.get(entry.0) == entry.1
}#sized-key-value-for-sortedmap-key-valuemethod size(self: SortedMap<Key, Value>): U64 {
return self.keyList.size()
}method isEmpty(self: SortedMap<Key, Value>): Boolean {
return self.keyList.isEmpty()
}#indexable-key-value-for-sortedmap-key-valuemethod get(self: SortedMap<Key, Value>, key: Key): Value? {
return self.get(key)
}method set(self: SortedMap<Key, Value>, key: Key, value: Value): Undefined {
self.set(key, value)
}#equatable-for-sortedmap-key-valuemethod equals(self: SortedMap<Key, Value>, other: SortedMap<Key, Value>): Boolean {
return self.keyList == other.keyList and self.valueList == other.valueList
}#hashable-for-sortedmap-key-valuemethod hash(self: SortedMap<Key, Value>): U64 {
return (self.keyList, self.valueList).hash()
}#tostring-for-sortedmap-key-valuemethod toString(self: SortedMap<Key, Value>): String {
return "{" ++ self.entries().map(λ((key, value)) key.toString() ++ ": " ++ value.toString()).join(", ") ++ "}"
}#default-for-sortedmap-key-valuefunction default(): SortedMap<Key, Value> {
return SortedMap<Key, Value>.new()
}#clone-for-sortedmap-key-valuemethod clone(self: SortedMap<Key, Value>): SortedMap<Key, Value> {
return SortedMap<Key, Value>{keyList := self.keyList.clone(), valueList := self.valueList.clone()}
}#trie-valueLayout
local function node for TrieNode<Value>(): TrieNode<Value> {
return TrieNode<Value>{children := SortedMap.new(), value := undefined, holds := false}
}global function new for Trie<Value>(): Trie<Value> {
return Trie<Value>{root := TrieNode<Value>.node(), count := 0}
}global function of for Trie<Value>(entries: ..(String, Value)): Trie<Value> {
return Trie<Value>.fromIterable(entries)
}/// the node key leads to - undefined when no key starts with it
local method find for Trie<Value>(self: Trie<Value>, key: String): TrieNode<Value>? {
return key.codepoints().reduce(self.root as TrieNode<Value>?, λ(node, c) node?.children[c])
}global method set for Trie<Value>(self: Trie<Value>, key: String, value: Value): Undefined {
let node := key.codepoints().reduce(self.root, λ(node, c) node.children.getOrInsert(c, λ TrieNode<Value>.node()))
if not node.holds then {
set self.count += 1
}
set node.value := value
set node.holds := true
}global method get for Trie<Value>(self: Trie<Value>, key: String): Value? {
return self.find(key)?.value
}global method has for Trie<Value>(self: Trie<Value>, key: String): Boolean {
return self.find(key)?.holds == true
}global method remove for Trie<Value>(self: Trie<Value>, key: String): Value? {
let node := self.find(key)
if node == undefined or not node.holds then {
return undefined
}
let value := node.value
set node.value := undefined
set node.holds := false
set self.count -= 1
return value
}global method hasPrefix for Trie<Value>(self: Trie<Value>, prefix: String): Boolean {
return self.withPrefix(prefix).next() != undefined
}/// every key at node or below it, prefix in front - shorter keys first, then by codepoint
local function below for Trie<Value>(node: TrieNode<Value>, prefix: String): Iterator<(String, Value)> {
if node.holds then {
yield (prefix, node.value!!)
}
for (c, child) in node.children {
for entry in below(child, prefix ++ c.toString()) {
yield entry
}
}
}global method withPrefix for Trie<Value>(self: Trie<Value>, prefix: String): Iterator<(String, Value)> {
let node := self.find(prefix)
return node == undefined ? Iterator.empty() : below(node, prefix)
}global method longestPrefixOf for Trie<Value>(self: Trie<Value>, text: String): String? {
let node := self.root
let best: String? := node.holds ? "" : undefined
for c, i in text.codepoints() {
set node := node.children[c] ?? (return best)
if node.holds then {
set best := text.slice(0..<i + 1)!!
}
}
return best
}global method keys for Trie<Value>(self: Trie<Value>): Iterator<String> {
return self.withPrefix("").map(λ((key, _)) key)
}global method clear for Trie<Value>(self: Trie<Value>): Undefined {
set self.root := TrieNode<Value>.node()
set self.count := 0
}#iterable-string-value-for-trie-valuemethod iterator(self: Trie<Value>): Iterator<(String, Value)> {
return self.withPrefix("")
}#fromiterable-string-value-for-trie-valuefunction fromIterable(entries: Iterable<(String, Value)>): Trie<Value> {
let trie := Trie<Value>.new()
for (key, value) in entries {
trie.set(key, value)
}
return trie
}#collection-string-value-for-trie-valuemethod contains(self: Trie<Value>, entry: (String, Value)): Boolean {
return self.has(entry.0) and self.get(entry.0) == entry.1
}#sized-string-value-for-trie-valuemethod size(self: Trie<Value>): U64 {
return self.count
}method isEmpty(self: Trie<Value>): Boolean {
return self.count == 0
}#indexable-string-value-for-trie-valuemethod get(self: Trie<Value>, key: String): Value? {
return self.get(key)
}method set(self: Trie<Value>, key: String, value: Value): Undefined {
self.set(key, value)
}#equatable-for-trie-valuemethod equals(self: Trie<Value>, other: Trie<Value>): Boolean {
return self.withPrefix("").collect() == other.withPrefix("").collect()
}#hashable-for-trie-valuemethod hash(self: Trie<Value>): U64 {
return self.withPrefix("").collect().hash()
}#tostring-for-trie-valuemethod toString(self: Trie<Value>): String {
return "Trie" ++ self.withPrefix("").collect().toString()
}#default-for-trie-valuefunction default(): Trie<Value> {
return Trie<Value>.new()
}#clone-for-trie-valuemethod clone(self: Trie<Value>): Trie<Value> {
return Trie<Value>.fromIterable(self)
}#lrucache-key-valueLayout
global function new for LruCache<Key: Hashable, Value>(capacity: U64): LruCache<Key, Value> {
return LruCache<Key, Value>{entries := Map.of(), order := [], limit := capacity.max(1)}
}local method touch for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key): Undefined {
self.order.removeWhere(λ(other) other == key)
self.order.push(key)
}global method get for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key): Value? {
return self.entries[key]
}global method use for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key): Value? {
if not self.entries.has(key) then {
return undefined
}
self.touch(key)
return self.entries[key]
}global method set for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key, value: Value): Undefined {
if not self.entries.has(key) and self.entries.size() >= self.limit then {
self.entries.remove(self.order.popFront()!!)
}
self.entries.set(key, value)
self.touch(key)
}global method has for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key): Boolean {
return self.entries.has(key)
}global method getOrInsert for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key, value: Lambda<(), Value>): Value {
if self.entries.has(key) then {
return self.use(key)!!
}
let made := value()
self.set(key, made)
return made
}global method remove for LruCache<Key, Value>(self: LruCache<Key, Value>, key: Key): Value? {
self.order.removeWhere(λ(other) other == key)
return self.entries.remove(key)
}global method capacity for LruCache<Key, Value>(self: LruCache<Key, Value>): U64 {
return self.limit
}global method entries for LruCache<Key, Value>(self: LruCache<Key, Value>): Iterator<(Key, Value)> {
return self.order.reverse().iterator().map(λ(key) (key, self.entries[key]!!))
}global method clear for LruCache<Key, Value>(self: LruCache<Key, Value>): Undefined {
self.entries.clear()
self.order.clear()
}#iterable-key-value-for-lrucache-key-valuemethod iterator(self: LruCache<Key, Value>): Iterator<(Key, Value)> {
return self.entries()
}#fromiterable-key-value-for-lrucache-key-valueas many entries as there are - each set in order, so the last is the most recently used
function fromIterable(entries: Iterable<(Key, Value)>): LruCache<Key, Value> {
let items := entries.toArray()
let cache := LruCache<Key, Value>.new(items.size())
for (key, value) in items {
cache.set(key, value)
}
return cache
}#collection-key-value-for-lrucache-key-valuemethod contains(self: LruCache<Key, Value>, entry: (Key, Value)): Boolean {
return self.entries.has(entry.0) and self.entries[entry.0] == entry.1
}#sized-key-value-for-lrucache-key-valuemethod size(self: LruCache<Key, Value>): U64 {
return self.entries.size()
}method isEmpty(self: LruCache<Key, Value>): Boolean {
return self.entries.isEmpty()
}#indexable-key-value-for-lrucache-key-valuemethod get(self: LruCache<Key, Value>, key: Key): Value? {
return self.get(key)
}method set(self: LruCache<Key, Value>, key: Key, value: Value): Undefined {
self.set(key, value)
}#equatable-for-lrucache-key-valuemethod equals(self: LruCache<Key, Value>, other: LruCache<Key, Value>): Boolean {
return self.entries().collect() == other.entries().collect()
}#hashable-for-lrucache-key-valuemethod hash(self: LruCache<Key, Value>): U64 {
return self.entries().collect().hash()
}#tostring-for-lrucache-key-valuemethod toString(self: LruCache<Key, Value>): String {
return "LruCache" ++ self.entries().collect().toString()
}#default-for-lrucache-key-valueone entry's worth
function default(): LruCache<Key, Value> {
return LruCache<Key, Value>.new(1)
}#clone-for-lrucache-key-valuemethod clone(self: LruCache<Key, Value>): LruCache<Key, Value> {
return LruCache<Key, Value>{entries := self.entries.clone(), order := self.order.clone(), limit := self.limit}
}#network-id-data-edgeadjacency both ways, so predecessors are as quick to find as successors - each edge's data kept on the outgoing side - Id: Hashable
Layout
local method missing for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): IndexError {
return IndexError.new("no node " ++ id.toString() ++ " in the network")
}local function empty for Network<Id, Data, Edge>(): Network<Id, Data, Edge> {
return Network<Id, Data, Edge>{data := Map.of(), outgoing := Map.of(), incoming := Map.of()}
}/// replaces the data of a node that's already there, keeping its edges
global method addNode for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id, data: Data): Undefined {
self.data.set(id, data)
self.outgoing.getOrInsert(id, λ Map.of())
self.incoming.getOrInsert(id, λ Set.of())
}global method removeNode for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Data? {
for next in self.successors(id).collect() {
self.disconnect(id, next)
}
for previous in self.predecessors(id).collect() {
self.disconnect(previous, id)
}
self.outgoing.remove(id)
self.incoming.remove(id)
return self.data.remove(id)
}global method hasNode for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Boolean {
return self.data.has(id)
}global method node for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Data? {
return self.data.get(id)
}global method nodes for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Iterator<Id> {
return self.data.keys()
}/// replaces the data of an edge that's already there
global method connect for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id, edge: Edge := undefined): IndexError ! Undefined {
if not self.hasNode(from) then { error self.missing(from) }
if not self.hasNode(to) then { error self.missing(to) }
self.outgoing.get(from)?.set(to, edge)
self.incoming.get(to)?.add(from)
}global method connectBoth for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, a: Id, b: Id, edge: Edge := undefined): IndexError ! Undefined {
self.connect(a, b, edge)!!
self.connect(b, a, edge)!!
}global method disconnect for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id): Boolean {
if not self.hasEdge(from, to) then { return false }
self.incoming.get(to)?.remove(from)
self.outgoing.get(from)?.remove(to)
return true
}global method edge for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id): Edge? {
return self.outgoing.get(from)?.get(to)
}global method hasEdge for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id): Boolean {
return self.outgoing.get(from)?.has(to) ?? false
}global method edges for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Iterator<(Id, Id)> {
for entry in self.outgoing {
let (from, targets) := entry
for to in targets.keys() {
yield (from, to)
}
}
}global method edgeEntries for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Iterator<(Id, Id, Edge)> {
for entry in self.outgoing {
let (from, targets) := entry
for target in targets {
let (to, edge) := target
yield (from, to, edge)
}
}
}global method successors for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Iterator<Id> {
return (self.outgoing.get(id) ?? Map.of()).keys()
}global method predecessors for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Iterator<Id> {
return (self.incoming.get(id) ?? Set.of()).iterator()
}global method neighbors for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): Iterator<Id> {
return self.successors(id).chain(self.predecessors(id)).distinct()
}global method inDegree for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): U64 {
return self.incoming.get(id)?.size() ?? 0
}global method outDegree for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, id: Id): U64 {
return self.outgoing.get(id)?.size() ?? 0
}/// nearest first - each node once, the first time it's reached
global method breadthFirst for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id): Iterator<Id> {
if not self.hasNode(from) then { return }
let seen := Set.of(from)
let waiting := Queue.fromIterable([from])
for _ in forever() while not waiting.isEmpty() {
let id := waiting.dequeue() as Id
yield id
for next in self.successors(id) if seen.add(next) {
waiting.enqueue(next)
}
}
}/// as deep as possible first - each node once, the first time it's reached
global method depthFirst for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id): Iterator<Id> {
if not self.hasNode(from) then { return }
let seen: Set<Id> := Set.of()
let waiting := Stack.fromIterable([from])
for _ in forever() while not waiting.isEmpty() {
let id := waiting.pop() as Id
if seen.add(id) then {
yield id
waiting.pushAll(self.successors(id).collect().reverse())
}
}
}/// breadth-first, remembering how each node was reached, then walked back from to
global method path for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id): Array<Id>? {
if not self.hasNode(from) or not self.hasNode(to) then { return undefined }
let cameFrom: Map<Id, Id> := Map.of()
let waiting := Queue.fromIterable([from])
let seen := Set.of(from)
for _ in forever() while not waiting.isEmpty() {
let id := waiting.dequeue() as Id
if id == to then {
let route := [to]
for _ in forever() while route.last() != from {
route.push(cameFrom.get(route.last() as Id) as Id)
}
return route.reverse()
}
for next in self.successors(id) if seen.add(next) {
cameFrom.set(next, id)
waiting.enqueue(next)
}
}
return undefined
}global method isReachable for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id): Boolean {
return self.breadthFirst(from).any(λ(id) id == to)
}global method hasCycle for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Boolean {
return self.topologicalSort() is Error
}/// Kahn's algorithm - repeatedly take a node nothing left points to; any left over are on a cycle
global method topologicalSort for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Error ! Array<Id> {
let remaining := Map.fromIterable(self.nodes().map(λ(id) (id, self.inDegree(id))))
let ready := Queue.fromIterable(self.nodes().filter(λ(id) self.inDegree(id) == 0))
let order: Array<Id> := []
for _ in forever() while not ready.isEmpty() {
let id := ready.dequeue() as Id
order.push(id)
for next in self.successors(id) {
remaining.update(next, λ(n) (n ?? 1) - 1)
if remaining.get(next) == 0 then { ready.enqueue(next) }
}
}
if order.size() != self.data.size() then { error Error.new("the network has a cycle") }
return order
}/// weakly connected - edges followed either way
global method components for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Array<Set<Id>> {
let seen: Set<Id> := Set.of()
let result: Array<Set<Id>> := []
for start in self.nodes() if not seen.has(start) {
let component := Set.of(start)
let waiting := Stack.fromIterable([start])
for _ in forever() while not waiting.isEmpty() {
for next in self.neighbors(waiting.pop() as Id) if component.add(next) {
waiting.push(next)
}
}
seen.addAll(component)
result.push(component)
}
return result
}global method subnetwork for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, ids: Set<Id>): Network<Id, Data, Edge> {
let result := Self.empty()
for id in ids if self.hasNode(id) {
result.addNode(id, self.node(id) as Data)
}
for entry in self.edgeEntries() if ids.has(entry[0]) and ids.has(entry[1]) {
let (from, to, edge) := entry
result.connect(from, to, edge) !? undefined
}
return result
}global method mapData for Network<Id, Data, Edge, Result>(self: Network<Id, Data, Edge>, f: Lambda<Data, Result>): Network<Id, Result, Edge> {
return Network<Id, Result, Edge>{data := self.data.mapValues(f), outgoing := self.outgoing.mapValues(λ(m) m.clone()), incoming := self.incoming.mapValues(λ(s) s.clone())}
}global method mapEdges for Network<Id, Data, Edge, Result>(self: Network<Id, Data, Edge>, f: Lambda<Edge, Result>): Network<Id, Data, Result> {
return Network<Id, Data, Result>{data := self.data.clone(), outgoing := self.outgoing.mapValues(λ(m) m.mapValues(f)), incoming := self.incoming.mapValues(λ(s) s.clone())}
}global method reverse for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Network<Id, Data, Edge> {
let result: Network<Id, Data, Edge> := Network.fromIterable(self.data.entries())
for entry in self.edgeEntries() {
let (from, to, edge) := entry
result.connect(to, from, edge) !? undefined
}
return result
}global method clear for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Undefined {
self.data.clear()
self.outgoing.clear()
self.incoming.clear()
}/// Tarjan's algorithm - each node's visit order and the lowest visit order reachable back from it; a node
/// whose own is the lowest closes a group: everything above it on the stack
global method stronglyConnected for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>): Array<Set<Id>> {
let order: Map<Id, U64> := Map.of()
let low: Map<Id, U64> := Map.of()
let stack: Array<Id> := []
let groups: Array<Set<Id>> := []
let visit := λ(id: Id) {
order.set(id, order.size())
low.set(id, order[id]!!)
stack.push(id)
for next in self.successors(id) {
if not order.has(next) then {
visit(next)
low.set(id, low[id]!!.min(low[next]!!))
} else if stack.contains(next) then {
low.set(id, low[id]!!.min(order[next]!!))
}
}
if low[id] == order[id] then {
let group: Set<Id> := Set.of()
for _ in forever() {
let top := stack.pop()!!
group.add(top)
if top == id then {
break _
}
}
groups.push(group)
}
}
for id in self.nodes() if not order.has(id) {
visit(id)
}
return groups.reverse()
}/// Dijkstra's search from `from` - A* with an estimate - stopping once `to` is settled: each node's
/// cheapest cost, and the node it was reached from
local method search for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id?, cost: Lambda<(Id, Id), F64>, estimate: Lambda<Id, F64>?): (Map<Id, F64>, Map<Id, Id>) {
let best: Map<Id, F64> := Map.of((from, 0.0))
let previous: Map<Id, Id> := Map.of()
let settled: Set<Id> := Set.of()
let queue := PriorityQueue<NetworkStep<Id>>.withOrder(λ(a, b) a.rank.compare(b.rank))
queue.push(NetworkStep{id := from, cost := 0.0, rank := estimate?.call(from) ?? 0.0})
for step in queue.drain() if settled.add(step.id) {
if step.id == to then {
break step
}
for next in self.successors(step.id) if not settled.contains(next) {
let reached := step.cost + cost(step.id, next)
if reached < best.getOr(next, F64.infinity()) then {
best.set(next, reached)
previous.set(next, step.id)
queue.push(NetworkStep{id := next, cost := reached, rank := reached + (estimate?.call(next) ?? 0.0)})
}
}
}
return (best, previous)
}global method cheapestPath for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, to: Id, cost: Lambda<(Id, Id), F64>, estimate: Lambda<Id, F64>? := undefined): Array<Id>? {
if not self.hasNode(from) then {
return undefined
}
let (best, previous) := self.search(from, to, cost, estimate)
if not best.has(to) then {
return undefined
}
let path := [to]
for _ in forever() while path.last() != from {
path.push(previous[path.last()!!]!!)
}
return path.reverse()
}global method distances for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, from: Id, cost: Lambda<(Id, Id), F64>): Map<Id, F64> {
return self.hasNode(from) ? self.search(from, undefined, cost, undefined).0 : Map.of()
}/// Kruskal's algorithm - the cheapest edges first, each kept when it joins two groups not yet joined
global method minimumSpanningTree for Network<Id, Data, Edge>(self: Network<Id, Data, Edge>, cost: Lambda<(Id, Id), F64>): Array<(Id, Id)> {
let group: Map<Id, Id> := Map.fromKeys(self.nodes(), λ(id) id)
let root := λ(id: Id) {
let at := id
for _ in forever() while group[at] != at {
set at := group[at]!!
}
return at
}
let tree: Array<(Id, Id)> := []
for (from, to) in self.edges().collect().sortedBy(λ((a, b)) cost(a, b)) if root(from) != root(to) {
group.set(root(from), root(to))
tree.push((from, to))
}
return tree
}#iterable-id-data-for-network-id-data-edgemethod iterator(self: Network<Id, Data, Edge>): Iterator<(Id, Data)> {
return self.data.entries()
}#fromiterable-id-data-for-network-id-data-edgethe nodes, with no edges between them
function fromIterable(items: Iterable<(Id, Data)>): Network<Id, Data, Edge> {
let result := Self.empty()
for entry in items {
let (id, value) := entry
result.addNode(id, value)
}
return result
}#collection-id-data-for-network-id-data-edgemethod contains(self: Network<Id, Data, Edge>, item: (Id, Data)): Boolean {
return self.data.contains(item)
}#sized-id-data-for-network-id-data-edgemethod size(self: Network<Id, Data, Edge>): U64 {
return self.data.size()
}method isEmpty(self: Network<Id, Data, Edge>): Boolean {
return self.data.isEmpty()
}#indexable-id-data-for-network-id-data-edgemethod get(self: Network<Id, Data, Edge>, key: Id): Data? {
return self.node(key)
}/// adds the node when it isn't there yet
method set(self: Network<Id, Data, Edge>, key: Id, value: Data): Error ! Undefined {
self.addNode(key, value)
}#equatable-for-network-id-data-edgeonly when Data and Edge are Equatable - the same nodes, edges and data on both
method equals(self: Network<Id, Data, Edge>, other: Network<Id, Data, Edge>): Boolean {
return self.data == other.data and self.outgoing == other.outgoing
}#hashable-for-network-id-data-edgeonly when Data is Hashable - order-independent, like equality
method hash(self: Network<Id, Data, Edge>): U64 {
return self.edges().reduce(self.data.hash(), λ(total, edge) total.wrappingAdd(hashCombine(edge[0].hash(), edge[1].hash())))
}#tostring-for-network-id-data-edge/// "Network{a: 1, b: 2; a -> b}"
method toString(self: Network<Id, Data, Edge>): String {
let edgeList := self.edges().map(λ(edge) edge[0].toString() ++ " -> " ++ edge[1].toString()).join(", ")
return "Network" ++ (self.data.toString().slice(0..<(self.data.toString().size() - 1)) !? "{") ++ "; " ++ edgeList ++ "}"
}#default-for-network-id-data-edgefunction default(): Network<Id, Data, Edge> {
return Self.empty()
}#clone-for-network-id-data-edgemethod clone(self: Network<Id, Data, Edge>): Network<Id, Data, Edge> {
return self.mapData(λ(value) value)
}#table-rowthe rows, in insertion order - Row: Struct
Layout
global function fromRows for Table<Row>(rows: Iterable<Row>): Table<Row> {
return Table<Row>{rows := rows.toArray()}
}/// every index built again from the rows - after anything that adds, removes or changes them
local method refreshIndexes for Table<Row>(self: Table<Row>): Undefined {
for field in self.indexes.keys().collect() {
self.addIndex(field)
}
}global method insert for Table<Row>(self: Table<Row>, row: Row): Undefined {
self.rows.push(row)
self.refreshIndexes()
}global method insertAll for Table<Row>(self: Table<Row>, rows: Iterable<Row>): Undefined {
set self.rows ++= rows
self.refreshIndexes()
}global method deleteWhere for Table<Row>(self: Table<Row>, where: Lambda<Row, Boolean>): U64 {
let removed := self.rows.removeWhere(where)
self.refreshIndexes()
return removed
}global method updateWhere for Table<Row>(self: Table<Row>, where: Lambda<Row, Boolean>, f: Lambda<Row, Row>): U64 {
let changed: U64 := 0
for row, i in self.rows if where(row) {
set self.rows[i] := f(row)
set changed += 1
}
self.refreshIndexes()
return changed
}global method where for Table<Row>(self: Table<Row>, keep: Lambda<Row, Boolean>): Table<Row> {
return Table.fromRows(self.rows.filter(keep))
}global method first for Table<Row>(self: Table<Row>, where: Lambda<Row, Boolean>): Row? {
return self.rows.find(where)
}global method select for Table<Row, Result: Struct>(self: Table<Row>, f: Lambda<Row, Result>): Table<Result> {
return Table.fromRows(self.rows.map(f))
}global method column for Table<Row, Field>(self: Table<Row>, name: String): Array<Field> {
return self.rows.map(λ(row) row.get(name) as Field)
}/// stable - rows with equal keys keep their order
global method orderBy for Table<Row, Key: Comparable>(self: Table<Row>, key: Lambda<Row, Key>, descending: Boolean := false): Table<Row> {
let sorted := self.rows.clone()
sorted.sortWith(λ(a, b) descending ? key(b).compare(key(a)) : key(a).compare(key(b)))
return Table.fromRows(sorted)
}global method groupBy for Table<Row, Key: Hashable>(self: Table<Row>, key: Lambda<Row, Key>): Map<Key, Table<Row>> {
return self.rows.groupBy(key).mapValues(λ(group) Table.fromRows(group))
}global method summarize for Table<Row, Key: Hashable, Result: Struct>(self: Table<Row>, key: Lambda<Row, Key>, f: Lambda<(Key, Table<Row>), Result>): Table<Result> {
let groups := self.rows.groupBy(key)
return Table.fromRows(self.rows.map(key).distinct().map(λ(k) f(k, Table.fromRows(groups.get(k) ?? []))))
}/// a hash join - other is indexed by its key once, then each row looks its matches up
global method join for Table<Row, Other, Key: Hashable, Result: Struct>(self: Table<Row>, other: Table<Other>, left: Lambda<Row, Key>, right: Lambda<Other, Key>, combine: Lambda<(Row, Other), Result>): Table<Result> {
let index := other.rows.groupBy(right)
let joined: Array<Result> := []
for row in self.rows {
for match in index.get(left(row)) ?? [] {
joined.push(combine(row, match))
}
}
return Table.fromRows(joined)
}/// join's hash join - a row with no matches still comes once, with undefined
global method leftJoin for Table<Row, Other, Key: Hashable, Result: Struct>(self: Table<Row>, other: Table<Other>, left: Lambda<Row, Key>, right: Lambda<Other, Key>, combine: Lambda<(Row, Other?), Result>): Table<Result> {
let index := other.rows.groupBy(right)
let joined: Array<Result> := []
for row in self.rows {
let matches := index.get(left(row))
if matches == undefined then {
joined.push(combine(row, undefined))
} else {
for match in matches {
joined.push(combine(row, match))
}
}
}
return Table.fromRows(joined)
}global method matching for Table<Row, Other, Key: Hashable>(self: Table<Row>, other: Table<Other>, left: Lambda<Row, Key>, right: Lambda<Other, Key>): Table<Row> {
let keys := Set.fromIterable(other.rows.map(right))
return self.where(λ(row) keys.contains(left(row)))
}global method notMatching for Table<Row, Other, Key: Hashable>(self: Table<Row>, other: Table<Other>, left: Lambda<Row, Key>, right: Lambda<Other, Key>): Table<Row> {
let keys := Set.fromIterable(other.rows.map(right))
return self.where(λ(row) not keys.contains(left(row)))
}global method distinct for Table<Row>(self: Table<Row>): Table<Row> {
return Table.fromRows(self.rows.distinct())
}global method distinctBy for Table<Row, Key: Hashable>(self: Table<Row>, key: Lambda<Row, Key>): Table<Row> {
let seen: Set<Key> := Set.of()
return self.where(λ(row) seen.add(key(row)))
}global method union for Table<Row>(self: Table<Row>, other: Table<Row>): Table<Row> {
return Table.fromRows((self.rows ++ other.rows).distinct())
}global method intersection for Table<Row>(self: Table<Row>, other: Table<Row>): Table<Row> {
let theirs := Set.fromIterable(other.rows)
return Table.fromRows(self.rows.filter(λ(row) theirs.contains(row)).distinct())
}global method difference for Table<Row>(self: Table<Row>, other: Table<Row>): Table<Row> {
let theirs := Set.fromIterable(other.rows)
return Table.fromRows(self.rows.filter(λ(row) not theirs.contains(row)).distinct())
}global method limit for Table<Row>(self: Table<Row>, count: U64): Table<Row> {
return Table.fromRows(self.rows.take(count))
}global method offset for Table<Row>(self: Table<Row>, count: U64): Table<Row> {
return Table.fromRows(self.rows.drop(count))
}/// each value of field, and the positions of the rows that have it - insert, deleteWhere, ... keep it
/// up to date
global method addIndex for Table<Row>(self: Table<Row>, field: String): Undefined {
let positions: Map<Unknown, Array<U64>> := Map.of()
for row, i in self.rows {
positions.getOrInsert(row.get(field), λ []).push(i)
}
self.indexes.set(field, positions)
}global method lookup for Table<Row, Value: Hashable>(self: Table<Row>, field: String, value: Value): Table<Row> {
let positions := self.indexes.get(field)
if positions == undefined then {
return self.where(λ(row) row.get(field) == value)
}
return Table.fromRows((positions.get(value) ?? []).map(λ(at) self.rows[at] as Row))
}global method withKey for Table<Row>(self: Table<Row>, key: String): Error ! Table<Row, key> {
let keyed := Table<Row, key>{rows := [], index := Map.of()}
for row in self.rows {
keyed.insert(row)!!
}
return keyed
}global method toArray for Table<Row>(self: Table<Row>): Array<Row> {
return self.rows.clone()
}global method columns for Table<Row>(self: Table<Row>): Array<String> {
return Row.fields().keys().collect()
}global method format for Table<Row>(self: Table<Row>): String {
let names := self.columns()
let cells := self.rows.map(λ(row) names.map(λ(name) row.get(name)))
let widths := names.map(λ(name) name.size())
for row in cells {
for cell, j in row {
set widths[j] := widths[j]!!.max(cell.toString().size())
}
}
let line := λ(texts: Array<String>) texts.join(" | ").trimEnd() ++ "\n"
let header := line(names.enumerate().map(λ((j, name)) name.padEnd(widths[j]!!)).toArray())
let rule := widths.map(λ(width) "-".repeat(width)).join("-+-") ++ "\n"
let body := cells.map(λ(row) line(row.enumerate().map(λ((j, cell)) cell is Numeric ? cell.toString().padStart(widths[j]!!) : cell.toString().padEnd(widths[j]!!)).toArray()))
return header ++ rule ++ body.join("")
}global method clear for Table<Row>(self: Table<Row>): Undefined {
self.rows.clear()
self.refreshIndexes()
}#iterable-row-for-table-rowmethod iterator(self: Table<Row>): Iterator<Row> {
return self.rows.iterator()
}#fromiterable-row-for-table-rowfunction fromIterable(items: Iterable<Row>): Table<Row> {
return Table.fromRows(items)
}#collection-row-for-table-rowmethod contains(self: Table<Row>, item: Row): Boolean {
return self.rows.contains(item)
}#sized-row-for-table-rowmethod size(self: Table<Row>): U64 {
return self.rows.size()
}method isEmpty(self: Table<Row>): Boolean {
return self.rows.isEmpty()
}#indexable-u64-row-for-table-rowmethod get(self: Table<Row>, key: U64): Row? {
return self.rows[key]
}method set(self: Table<Row>, key: U64, value: Row): Error ! Undefined {
set self.rows[key] := value
self.refreshIndexes()
}#equatable-for-table-rowonly when Row is Equatable
method equals(self: Table<Row>, other: Table<Row>): Boolean {
return self.rows == other.rows
}#hashable-for-table-rowonly when Row is Hashable
method hash(self: Table<Row>): U64 {
return self.rows.hash()
}#tostring-for-table-row/// one row per line, fields separated by " | ", under a line of column names
method toString(self: Table<Row>): String {
let header := self.columns().join(" | ")
let lines := self.rows.map(λ(row) self.columns().map(λ(name) row.get(name)?.toString() ?? "").join(" | "))
return [header, ..lines].join("\n")
}#concatenable-for-table-rowself's rows, then other's
method concat(self: Table<Row>, other: Table<Row>): Table<Row> {
return Table<Row>{rows := self.rows ++ other.rows}
}#default-for-table-rowfunction default(): Table<Row> {
return Table<Row>{rows := []}
}#clone-for-table-rowmethod clone(self: Table<Row>): Table<Row> {
return Table<Row>{rows := self.rows.clone()}
}#table-row-keya Table
Layout
/// key is the Table's value parameter - the key field's name - read here, since the arguments below
/// named key shadow it
local function keyOf for Table<Row, key>(row: Row): Key {
return row.get(key) as Key
}/// the key index and addIndex's indexes, built again from the rows
local method reindex for Table<Row, key>(self: Table<Row, key>): Error ! Undefined {
self.refreshIndexes()
self.index.clear()
for row, i in self.rows {
if self.index.has(Self.keyOf(row)) then { error Error.new("two rows share the key " ++ Self.keyOf(row).toString()) }
self.index.set(Self.keyOf(row), i)
}
}global method has for Table<Row, key>(self: Table<Row, key>, key: Key): Boolean {
return self.index.has(key)
}global method insert for Table<Row, key>(self: Table<Row, key>, row: Row): Error ! Undefined {
if self.index.has(Self.keyOf(row)) then { error Error.new("a row with the key " ++ Self.keyOf(row).toString() ++ " is already there") }
self.index.set(Self.keyOf(row), self.rows.size())
self.rows.push(row)
self.refreshIndexes()
}global method upsert for Table<Row, key>(self: Table<Row, key>, row: Row): Undefined {
let at := self.index.get(Self.keyOf(row))
if at == undefined then {
self.insert(row) !? undefined
} else {
set self.rows[at] := row
self.refreshIndexes()
}
}/// f may change the key too - the index follows it
global method update for Table<Row, key>(self: Table<Row, key>, key: Key, f: Lambda<Row, Row>): IndexError ! Undefined {
let at := (self.index.get(key) ?! IndexError.new("no row with the key " ++ key.toString()))!!
let changed := f(self.rows[at] as Row)
self.index.remove(key)
self.index.set(Self.keyOf(changed), at)
set self.rows[at] := changed
self.refreshIndexes()
}global method delete for Table<Row, key>(self: Table<Row, key>, key: Key): Row? {
let at := self.index.get(key)
if at == undefined then { return undefined }
let row := self.rows.removeAt(at) !? undefined
self.reindex() !? undefined
return row
}global method keys for Table<Row, key>(self: Table<Row, key>): Iterator<Key> {
return self.rows.iterator().map(λ(row) Self.keyOf(row))
}global method withoutKey for Table<Row, key>(self: Table<Row, key>): Table<Row> {
return Table<Row>{rows := self.rows.clone()}
}global method insertAll for Table<Row, key>(self: Table<Row, key>, rows: Iterable<Row>): Error ! Undefined {
for row in rows {
self.insert(row)!!
}
}global method deleteWhere for Table<Row, key>(self: Table<Row, key>, where: Lambda<Row, Boolean>): U64 {
let removed := self.rows.removeWhere(where)
self.reindex() !? undefined
return removed
}/// a change that makes two keys equal is an error, and undoes nothing before it - check first when
/// that matters
global method updateWhere for Table<Row, key>(self: Table<Row, key>, where: Lambda<Row, Boolean>, f: Lambda<Row, Row>): U64 {
let changed: U64 := 0
for row, i in self.rows if where(row) {
set self.rows[i] := f(row)
set changed += 1
}
self.reindex() !? undefined
return changed
}#indexable-key-row-for-table-row-keymethod get(self: Table<Row, key>, key: Key): Row? {
let at := self.index.get(key)
return at == undefined ? undefined : self.rows[at]
}/// replaces the row with that key, or adds it - value's own key field has to match
method set(self: Table<Row, key>, key: Key, value: Row): Error ! Undefined {
if Self.keyOf(value) != key then { error Error.new("the row's key " ++ Self.keyOf(value).toString() ++ " isn't " ++ key.toString()) }
self.upsert(value)
}#vector-type-sizesize values, in order - Type: Numeric
Layout
local method item for Vector<Type, size>(self: Vector<Type, size>, index: U64): Type {
return self.items[index] ?? Type.zero()
}global function filled for Vector<Type, size>(value: Type): Self {
return Self{items := Array.filled(size, value)}
}global function unit for Vector<Type, size>(axis: U64): IndexError ! Self {
if axis >= size then { error IndexError.new("axis " ++ axis.toString() ++ " is past a " ++ size.toString() ++ "-dimensional vector") }
return Self{items := Array.generate(size, λ(i) i == axis ? Type.one() : Type.zero())}
}global function fromArray for Vector<Type, size>(items: Array<Type>): Error ! Self {
if items.size() != size then { error Error.new("a Vector of size " ++ size.toString() ++ " needs " ++ size.toString() ++ " items, got " ++ items.size().toString()) }
return Self{items := items.clone()}
}global method x for Vector<Type, size>(self: Vector<Type, size>): Type {
return self.item(0)
}global method y for Vector<Type, size>(self: Vector<Type, size>): Type {
return self.item(1)
}global method z for Vector<Type, size>(self: Vector<Type, size>): Type {
return self.item(2)
}global method w for Vector<Type, size>(self: Vector<Type, size>): Type {
return self.item(3)
}global method dot for Vector<Type, size>(self: Vector<Type, size>, other: Self): Type {
return self.zipWith(other, λ(a, b) a * b).sum()
}/// size is 3
global method cross for Vector<Type, size>(self: Vector<Type, size>, other: Self): Self {
return Self{items := [
self.y() * other.z() - self.z() * other.y(),
self.z() * other.x() - self.x() * other.z(),
self.x() * other.y() - self.y() * other.x(),
]}
}global method magnitude for Vector<Type, size>(self: Vector<Type, size>): Float {
return (self.magnitudeSquared() as Float).sqrt()
}global method magnitudeSquared for Vector<Type, size>(self: Vector<Type, size>): Type {
return self.dot(self)
}global method normalize for Vector<Type, size>(self: Vector<Type, size>): Vector<Float, size> {
let length := self.magnitude()
return self.map(λ(v) (v as Float) / length)
}global method distance for Vector<Type, size>(self: Vector<Type, size>, other: Self): Float {
return (self - other).magnitude()
}global method angle for Vector<Type, size>(self: Vector<Type, size>, other: Self): Float {
let cosine := (self.dot(other) as Float) / (self.magnitude() * other.magnitude())
return cosine.clamp(-1.0..=1.0).acos()
}global method project for Vector<Type, size>(self: Vector<Type, size>, onto: Self): Vector<Float, size> {
let factor := (self.dot(onto) as Float) / (onto.magnitudeSquared() as Float)
return onto.map(λ(v) (v as Float) * factor)
}global method lerp for Vector<Type, size>(self: Vector<Type, size>, to: Self, t: Float): Vector<Float, size> {
return self.zipWith(to, λ(a, b) (a as Float).lerp(b as Float, t))
}global method map for Vector<Type, size, Result>(self: Vector<Type, size>, f: Lambda<Type, Result>): Vector<Result, size> {
return Vector<Result, size>{items := self.items.map(f)}
}global method zipWith for Vector<Type, size, Result>(self: Vector<Type, size>, other: Self, f: Lambda<(Type, Type), Result>): Vector<Result, size> {
return Vector<Result, size>{items := Array.generate(size, λ(i) f(self.item(i), other.item(i)))}
}global method toArray for Vector<Type, size>(self: Vector<Type, size>): Array<Type> {
return self.items.clone()
}global method toRow for Vector<Type, size>(self: Vector<Type, size>): Matrix<Type, 1, size> {
return Matrix<Type, 1, size>.fromRows([self.items]) !? Matrix<Type, 1, size>.zero()
}global method toColumn for Vector<Type, size>(self: Vector<Type, size>): Matrix<Type, size, 1> {
return Matrix<Type, size, 1>.fromColumns([self.items]) !? Matrix<Type, size, 1>.zero()
}#equatable-for-vector-type-sizemethod equals(self: Vector<Type, size>, other: Self): Boolean {
return self.items == other.items
}#hashable-for-vector-type-sizemethod hash(self: Vector<Type, size>): U64 {
return self.items.hash()
}#tostring-for-vector-type-size/// "(1, 2, 3)"
method toString(self: Vector<Type, size>): String {
return "(" ++ self.items.join(", ") ++ ")"
}#numeric-for-vector-type-sizeelement by element - zero and one fill every place
function zero(): Self {
return Self.filled(Type.zero())
}function one(): Self {
return Self.filled(Type.one())
}method add(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a + b)
}method subtract(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a - b)
}method multiply(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a * b)
}method divide(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a / b)
}method floorDivide(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a // b)
}method remainder(self: Vector<Type, size>, other: Self): Self {
return self.zipWith(other, λ(a, b) a % b)
}method power(self: Vector<Type, size>, exponent: Self): Self {
return self.zipWith(exponent, λ(a, b) a ^ b)
}method negate(self: Vector<Type, size>): Self {
return self.map(λ(v) -v)
}method plus(self: Vector<Type, size>): Self {
return self
}#scalable-type-for-vector-type-sizemethod scale(self: Vector<Type, size>, factor: Type): Self {
return self.map(λ(v) v * factor)
}#iterable-type-for-vector-type-sizemethod iterator(self: Vector<Type, size>): Iterator<Type> {
return self.items.iterator()
}#sized-type-for-vector-type-sizemethod size(self: Vector<Type, size>): U64 {
return size
}method isEmpty(self: Vector<Type, size>): Boolean {
return size == 0
}#indexable-u64-type-for-vector-type-sizemethod get(self: Vector<Type, size>, key: U64): Type? {
return self.items[key]
}method set(self: Vector<Type, size>, key: U64, value: Type): Error ! Undefined {
set self.items[key] := value
}#default-for-vector-type-sizefunction default(): Self {
return Self.zero()
}#copy-for-vector-type-sizea copy holds its own items
Nothing to write - a marker interface, or one another block already covers.
#matrix-type-rows-colsrows * cols values, row by row - Type: Numeric
Layout
local method cell for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, r: U64, c: U64): Type {
return self.cells[r * cols + c] ?? Type.zero()
}local function build for Matrix<Type, rows, cols>(f: Lambda<(U64, U64), Type>): Self {
return Self{cells := Array.generate(rows * cols, λ(i) f(i / cols, i % cols))}
}/// the same values as Floats, for the algorithms that divide
local method toFloats for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Array<Array<Float>> {
return Array.generate(rows, λ(r) Array.generate(cols, λ(c) self.cell(r, c) as Float))
}local function sizeError for Matrix<Type, rows, cols>(what: String): Error {
return Error.new("a " ++ rows.toString() ++ "x" ++ cols.toString() ++ " matrix needs " ++ what)
}/// rows == cols
global function identity for Matrix<Type, rows, cols>(): Self {
return Self.build(λ(r, c) r == c ? Type.one() : Type.zero())
}global function filled for Matrix<Type, rows, cols>(value: Type): Self {
return Self{cells := Array.filled(rows * cols, value)}
}global function fromRows for Matrix<Type, rows, cols>(rows: Array<Array<Type>>): Error ! Self {
if rows.size() != Self.rows or rows.any(λ(row) row.size() != cols) then { error Self.sizeError(Self.rows.toString() ++ " rows of " ++ cols.toString()) }
return Self{cells := rows.flatten()}
}global function fromColumns for Matrix<Type, rows, cols>(columns: Array<Array<Type>>): Error ! Self {
if columns.size() != cols or columns.any(λ(column) column.size() != rows) then { error Self.sizeError(cols.toString() ++ " columns of " ++ rows.toString()) }
return Self.build(λ(r, c) columns[c]?[r] ?? Type.zero())
}/// rows == cols
global function diagonal for Matrix<Type, rows, cols>(values: Vector<Type, rows>): Self {
return Self.build(λ(r, c) r == c ? values[r] ?? Type.zero() : Type.zero())
}global method row for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, index: U64): IndexError ! Vector<Type, cols> {
if index >= rows then { error IndexError.new("row " ++ index.toString() ++ " is past " ++ rows.toString() ++ " rows") }
return Vector<Type, cols>{items := Array.generate(cols, λ(c) self.cell(index, c))}
}global method column for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, index: U64): IndexError ! Vector<Type, rows> {
if index >= cols then { error IndexError.new("column " ++ index.toString() ++ " is past " ++ cols.toString() ++ " columns") }
return Vector<Type, rows>{items := Array.generate(rows, λ(r) self.cell(r, index))}
}global method rows for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Array<Vector<Type, cols>> {
return Array.generate(rows, λ(r) self.row(r) !? Vector<Type, cols>.zero())
}global method columns for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Array<Vector<Type, rows>> {
return Array.generate(cols, λ(c) self.column(c) !? Vector<Type, rows>.zero())
}global method transpose for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Matrix<Type, cols, rows> {
return Matrix<Type, cols, rows>{cells := Array.generate(rows * cols, λ(i) self.cell(i % rows, i / rows))}
}global method multiply for Matrix<Type, rows, cols, Other>(self: Matrix<Type, rows, cols>, other: Matrix<Type, cols, Other>): Matrix<Type, rows, Other> {
return Matrix<Type, rows, Other>{cells := Array.generate(rows * Other, λ(i) (0..<cols).toArray().map(λ(k) self.cell(i / Other, k) * other.cell(k, i % Other)).sum())}
}global method apply for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, vector: Vector<Type, cols>): Vector<Type, rows> {
return Vector<Type, rows>{items := Array.generate(rows, λ(r) (self.row(r) !? Vector<Type, cols>.zero()).dot(vector))}
}/// rows == cols - Bareiss elimination: every division is exact, so an integer matrix stays in integers
global method determinant for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Type {
if rows == 0 then { return Type.one() }
let m := Array.generate(rows, λ(r) Array.generate(cols, λ(c) self.cell(r, c)))
let sign := Type.one()
let previous := Type.one()
for k in 0..<(rows - 1) {
if m[k]?[k] == Type.zero() then {
let swapWith := ((k + 1)..<rows).find(λ(r) m[r]?[k] != Type.zero())
if swapWith == undefined then { return Type.zero() }
m.swap(k, swapWith) !? undefined
set sign := -sign
}
let pivot := m[k]?[k] ?? Type.one()
for i in (k + 1)..<rows {
for j in (k + 1)..<rows {
set m[i][j] := ((m[i]?[j] ?? Type.zero()) * pivot - (m[i]?[k] ?? Type.zero()) * (m[k]?[j] ?? Type.zero())) / previous
}
}
set previous := pivot
}
return sign * (m[rows - 1]?[rows - 1] ?? Type.zero())
}/// Gauss-Jordan on [self | identity], choosing the largest pivot in each column for stability
global method inverse for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Error ! Matrix<Float, rows, cols> {
if rows != cols then { error Self.sizeError("to be square to have an inverse") }
let m := self.toFloats()
let result := Array.generate(rows, λ(r) Array.generate(rows, λ(c) r == c ? 1.0 : 0.0))
for k in 0..<rows {
let pivotRow := (k..<rows).toArray().maxBy(λ(r) (m[r]?[k] ?? 0.0).abs()) !? k
if (m[pivotRow]?[k] ?? 0.0).abs() < Float.epsilon() then { error Error.new("the matrix is singular") }
m.swap(k, pivotRow) !? undefined
result.swap(k, pivotRow) !? undefined
let pivot := m[k]?[k] ?? 1.0
for j in 0..<rows {
set m[k][j] /= pivot
set result[k][j] /= pivot
}
for i in 0..<rows if i != k {
let factor := m[i]?[k] ?? 0.0
for j in 0..<rows {
set m[i][j] -= factor * (m[k]?[j] ?? 0.0)
set result[i][j] -= factor * (result[k]?[j] ?? 0.0)
}
}
}
return Matrix<Float, rows, cols>.fromRows(result)!!
}/// rows == cols
global method trace for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Type {
return (0..<rows).toArray().map(λ(i) self.cell(i, i)).sum()
}/// row reduction in Floats - a pivot within epsilon of zero counts as zero
global method rank for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): U64 {
let m := self.toFloats()
let rank: U64 := 0
for c in 0..<cols while rank < rows {
let pivotRow := (rank..<rows).toArray().maxBy(λ(r) (m[r]?[c] ?? 0.0).abs()) !? rank
if (m[pivotRow]?[c] ?? 0.0).abs() > Float.epsilon() * 1000 then {
m.swap(rank, pivotRow) !? undefined
for i in (rank + 1)..<rows {
let factor := (m[i]?[c] ?? 0.0) / (m[rank]?[c] ?? 1.0)
for j in c..<cols {
set m[i][j] -= factor * (m[rank]?[j] ?? 0.0)
}
}
set rank += 1
}
}
return rank
}/// rows == cols - square-and-multiply
global method power for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, exponent: U64): Self {
let result := Self.identity()
let base := self
let remaining := exponent
for _ in forever() while remaining > 0 {
if remaining.isOdd() then { set result := result.multiply(base) }
set remaining := remaining shr 1
if remaining > 0 then { set base := base.multiply(base) }
}
return result
}global method isSquare for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Boolean {
return rows == cols
}global method isSymmetric for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Boolean {
return self.isSquare() and (0..<rows).all(λ(r) (0..<r).all(λ(c) self.cell(r, c) == self.cell(c, r)))
}global method map for Matrix<Type, rows, cols, Result>(self: Matrix<Type, rows, cols>, f: Lambda<Type, Result>): Matrix<Result, rows, cols> {
return Matrix<Result, rows, cols>{cells := self.cells.map(f)}
}global method submatrix for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>, rowRange: Range<U64>, colRange: Range<U64>): IndexError ! Matrix<Type, ?, ?> {
let (top, bottom) := bounds(rowRange)
let (left, right) := bounds(colRange)
if bottom > rows or right > cols then { error IndexError.new("the submatrix reaches past the matrix") }
return Matrix<Type, bottom - top, right - left>{cells := Array.generate((bottom - top) * (right - left), λ(i) self.cell(top + i / (right - left), left + i % (right - left)))}
}global method toArray for Matrix<Type, rows, cols>(self: Matrix<Type, rows, cols>): Array<Array<Type>> {
return Array.generate(rows, λ(r) Array.generate(cols, λ(c) self.cell(r, c)))
}#equatable-for-matrix-type-rows-colsmethod equals(self: Matrix<Type, rows, cols>, other: Self): Boolean {
return self.cells == other.cells
}#hashable-for-matrix-type-rows-colsmethod hash(self: Matrix<Type, rows, cols>): U64 {
return self.cells.hash()
}#tostring-for-matrix-type-rows-cols/// one row per line - "[1, 2]\n[3, 4]"
method toString(self: Matrix<Type, rows, cols>): String {
return self.toArray().map(λ(row) row.toString()).join("\n")
}#numeric-for-matrix-type-rows-colsmultiply is the matrix product, and one is the identity - both square only; the rest go element by element
function zero(): Self {
return Self.filled(Type.zero())
}function one(): Self {
return Self.identity()
}method add(self: Matrix<Type, rows, cols>, other: Self): Self {
return Self.build(λ(r, c) self.cell(r, c) + other.cell(r, c))
}method subtract(self: Matrix<Type, rows, cols>, other: Self): Self {
return Self.build(λ(r, c) self.cell(r, c) - other.cell(r, c))
}method multiply(self: Matrix<Type, rows, cols>, other: Self): Self {
return self.multiply<cols>(other)
}/// self times other's inverse
method divide(self: Matrix<Type, rows, cols>, other: Self): Self {
let inverse := other.inverse() !? Matrix<Float, rows, cols>.zero()
return self.map(λ(v) v as Float).multiply<cols>(inverse).map(λ(v) v as Type)
}method floorDivide(self: Matrix<Type, rows, cols>, other: Self): Self {
return Self.build(λ(r, c) self.cell(r, c) // other.cell(r, c))
}method remainder(self: Matrix<Type, rows, cols>, other: Self): Self {
return Self.build(λ(r, c) self.cell(r, c) % other.cell(r, c))
}method power(self: Matrix<Type, rows, cols>, exponent: Self): Self {
return Self.build(λ(r, c) self.cell(r, c) ^ exponent.cell(r, c))
}method negate(self: Matrix<Type, rows, cols>): Self {
return self.map(λ(v) -v)
}method plus(self: Matrix<Type, rows, cols>): Self {
return self
}#scalable-type-for-matrix-type-rows-colsmethod scale(self: Matrix<Type, rows, cols>, factor: Type): Self {
return self.map(λ(v) v * factor)
}#indexable-u64-u64-type-for-matrix-type-rows-colsmethod get(self: Matrix<Type, rows, cols>, key: (U64, U64)): Type? {
let (r, c) := key
return r < rows and c < cols ? self.cell(r, c) : undefined
}method set(self: Matrix<Type, rows, cols>, key: (U64, U64), value: Type): Error ! Undefined {
let (r, c) := key
if r >= rows or c >= cols then { error IndexError.new("(" ++ r.toString() ++ ", " ++ c.toString() ++ ") is outside the matrix") }
set self.cells[r * cols + c] := value
}#default-for-matrix-type-rows-colsfunction default(): Self {
return Self.zero()
}#copy-for-matrix-type-rows-colsa copy holds its own cells
Nothing to write - a marker interface, or one another block already covers.
#colorLayout
local method channel for Color(self: Color, shift: U64): U8 {
return (self.rgba.shr(shift) band 0xFF) as U8
}/// an amount from 0 to 1 as a channel, rounded to the nearest - clamped when outside
local function toChannel for Color(amount: F64): U8 {
return (amount.max(0.0).min(1.0) * 255.0).round() as U8
}global function rgba for Color(red: U8, green: U8, blue: U8, alpha: U8): Color {
return Color{rgba := (red as U32).shl(24) bor (green as U32).shl(16) bor (blue as U32).shl(8) bor (alpha as U32)}
}global function rgb for Color(red: U8, green: U8, blue: U8): Color {
return Color.rgba(red, green, blue, 255)
}global function gray for Color(value: U8): Color {
return Color.rgba(value, value, value, 255)
}/// which sixth of the circle the hue is in picks the channel that leads and the one that trails
global function fromHsv for Color(hue: F64, saturation: F64, value: F64): Color {
let h := (hue % 360.0 + 360.0) % 360.0 / 60.0
let c := value * saturation
let x := c * (1.0 - (h % 2.0 - 1.0).abs())
let m := value - c
let (r, g, b) := h < 1.0 ? (c, x, 0.0) : h < 2.0 ? (x, c, 0.0) : h < 3.0 ? (0.0, c, x) : h < 4.0 ? (0.0, x, c) : h < 5.0 ? (x, 0.0, c) : (c, 0.0, x)
return Color.rgb(Color.toChannel(r + m), Color.toChannel(g + m), Color.toChannel(b + m))
}global method red for Color(self: Color): U8 {
return self.channel(24)
}global method green for Color(self: Color): U8 {
return self.channel(16)
}global method blue for Color(self: Color): U8 {
return self.channel(8)
}global method alpha for Color(self: Color): U8 {
return self.channel(0)
}global method withAlpha for Color(self: Color, alpha: U8): Color {
return Color{rgba := (self.rgba band 0xFFFFFF00) bor (alpha as U32)}
}global method lerp for Color(self: Color, to: Color, t: F64): Color {
let mix := λ(a, b) Color.toChannel(((a as F64) + ((b as F64) - (a as F64)) * t) / 255.0)
return Color.rgba(mix(self.red(), to.red()), mix(self.green(), to.green()), mix(self.blue(), to.blue()), mix(self.alpha(), to.alpha()))
}/// source-over: the result's alpha is a + b(1 - a), and each channel is weighted by the alpha it comes with
global method over for Color(self: Color, background: Color): Color {
let a := (self.alpha() as F64) / 255.0
let b := (background.alpha() as F64) / 255.0
let total := a + b * (1.0 - a)
if total == 0.0 then { return Color.default() }
let mix := λ(top, bottom) Color.toChannel(((top as F64) * a + (bottom as F64) * b * (1.0 - a)) / total / 255.0)
return Color.rgba(mix(self.red(), background.red()), mix(self.green(), background.green()), mix(self.blue(), background.blue()), Color.toChannel(total))
}global method luminance for Color(self: Color): F64 {
return (0.2126 * (self.red() as F64) + 0.7152 * (self.green() as F64) + 0.0722 * (self.blue() as F64)) / 255.0
}global method toHex for Color(self: Color): String {
let digits := self.rgba.toString(16).padStart(8, '0')
return "#" ++ (self.alpha() == 255 ? (digits.slice(0..<6) !? digits) : digits)
}#equatable-for-colormethod equals(self: Color, other: Color): Boolean {
return self.rgba == other.rgba
}#hashable-for-colormethod hash(self: Color): U64 {
return hashCombine(&hashSeed, self.rgba as U64)
}#tostring-for-colormethod toString(self: Color): String {
return self.toHex()
}#fromstring-for-color/// "#rgb" and "#rgba" double each digit - "#f80" is "#ff8800"; without alpha it's opaque
function parse(text: String): ParseError ! Color {
let digits := text.trim().stripPrefix("#") ?? text.trim()
let full := digits.size() <= 4 ? digits.codepoints().map(λ(c) c.toString().repeat(2)).join() : digits
if full.size() != 6 and full.size() != 8 then { error ParseError.new("a color is written #rgb, #rgba, #rrggbb or #rrggbbaa, got \"" ++ text ++ "\"") }
let value := U32.parse(full, 16)!!
return Color{rgba := full.size() == 6 ? value.shl(8) bor 0xFF : value}
}#default-for-color/// transparent black
function default(): Color {
return Color{rgba := 0}
}#copy-for-colorNothing to write - a marker interface, or one another block already covers.
#imageLayout
uses #builtin.bytesUint
local method at for Image(self: Image, x: U64, y: U64): Color {
return Color{#builtin.bytesUint(self.rgba, (y * self.columns + x) * 4, 4, true) as U32}
}local method check for Image(self: Image, x: U64, y: U64): IndexError ! Undefined {
if x >= self.columns or y >= self.rows then {
error IndexError.new(#format"(#{ x }, #{ y }) is outside a #{ self.columns }x#{ self.rows } image")
}
}global function new for Image(width: U64, height: U64, fill: Color := Color.default()): Image {
return Image{width, height, #builtin.pixelsFilled(width * height, fill.rgba as F64)}
}global function generate for Image(width: U64, height: U64, f: Lambda<(U64, U64), Color>): Image {
let rgba := Bytes.filled(width * height * 4, 0)
for y in 0..<height {
for x in 0..<width {
#builtin.bytesPutUint(rgba, (y * width + x) * 4, 4, f(x, y).rgba as F64, true)
}
}
return Image{width, height, rgba}
}global function fromPixels for Image(width: U64, height: U64, pixels: Array<Color>): Error ! Image {
if pixels.size() != width * height then {
error Error.new(#format"a #{ width }x#{ height } image needs #{ width * height } pixels, got #{ pixels.size() }")
}
let rgba := Bytes.filled(width * height * 4, 0)
for color, i in pixels {
#builtin.bytesPutUint(rgba, i * 4, 4, color.rgba as F64, true)
}
return Image{width, height, rgba}
}global function fromRgba for Image(width: U64, height: U64, rgba: Bytes): Error ! Image {
if rgba.size() != width * height * 4 then {
error Error.new(#format"a #{ width }x#{ height } image needs #{ width * height * 4 } bytes, got #{ rgba.size() }")
}
return Image{width, height, rgba.slice(0..<rgba.size())!!}
}global method width for Image(self: Image): U64 {
return self.columns
}global method height for Image(self: Image): U64 {
return self.rows
}global method pixel for Image(self: Image, x: U64, y: U64): IndexError ! Color {
self.check(x, y)!!
return self.at(x, y)
}global method setPixel for Image(self: Image, x: U64, y: U64, color: Color): IndexError ! Undefined {
self.check(x, y)!!
#builtin.bytesPutUint(self.rgba, (y * self.columns + x) * 4, 4, color.rgba as F64, true)
} uses #builtin.bytesUint
global method pixels for Image(self: Image): Array<Color> {
return Array.generate(self.columns * self.rows, λ(i) Color{#builtin.bytesUint(self.rgba, i * 4, 4, true) as U32})
}global method toRgba for Image(self: Image): Bytes {
return self.rgba.slice(0..<self.rgba.size())!!
}global method fill for Image(self: Image, color: Color): Undefined {
set self.rgba := #builtin.pixelsFilled(self.columns * self.rows, color.rgba as F64)
} uses #builtin.bytesPutUint, #builtin.bytesUint
global method map for Image(self: Image, f: Lambda<Color, Color>): Image {
let rgba := Bytes.filled(self.rgba.size(), 0)
for i in 0..<(self.columns * self.rows) {
#builtin.bytesPutUint(rgba, i * 4, 4, f(Color{#builtin.bytesUint(self.rgba, i * 4, 4, true) as U32}).rgba as F64, true)
}
return Image{self.columns, self.rows, rgba}
} uses #builtin.pixelsCrop
global method crop for Image(self: Image, x: U64, y: U64, width: U64, height: U64): IndexError ! Image {
if x + width > self.columns or y + height > self.rows then {
error IndexError.new(#format"a #{ width }x#{ height } region at (#{ x }, #{ y }) doesn't fit in a #{ self.columns }x#{ self.rows } image")
}
return Image{width, height, #builtin.pixelsCrop(self.rgba, self.columns, x, y, width, height)}
}/// nearest neighbour - each new pixel the old one its top-left corner lands in; smooth averages the
/// pixels each new one covers when shrinking, and blends the four nearest (bilinear) when growing
global method resize for Image(self: Image, width: U64, height: U64, smooth: Boolean := false): Image {
if self.columns == 0 or self.rows == 0 then {
return Image.new(width, height)
}
return Image{width, height, #builtin.pixelsResize(self.rgba, self.columns, self.rows, width, height, smooth)}
} uses #builtin.pixelsFlip
global method flipHorizontal for Image(self: Image): Image {
return Image{self.columns, self.rows, #builtin.pixelsFlip(self.rgba, self.columns, self.rows, true)}
} uses #builtin.pixelsFlip
global method flipVertical for Image(self: Image): Image {
return Image{self.columns, self.rows, #builtin.pixelsFlip(self.rgba, self.columns, self.rows, false)}
}/// a quarter turn clockwise makes the old left column the new top row, read bottom to top
global method rotate for Image(self: Image, quarterTurns: U8): Image {
let turns := (quarterTurns % 4) as U64
if turns == 0 then {
return Image{self.columns, self.rows, self.toRgba()}
}
let swapped := turns != 2
return Image{if swapped then self.rows else self.columns, if swapped then self.columns else self.rows, #builtin.pixelsRotate(self.rgba, self.columns, self.rows, turns)}
} uses #builtin.pixelsDraw
global method draw for Image(self: Image, source: Image, x: S64, y: S64): Undefined {
#builtin.pixelsDraw(self.rgba, self.columns, self.rows, source.rgba, source.columns, source.rows, x, y)
}/// the pixel layouts pixelsUnpack and pixelsPack know (see media.wat's Pixels)
local const LayoutRgba: U64 := 0local const LayoutRgb: U64 := 1local const LayoutBgra: U64 := 2local const LayoutBgr: U64 := 3local const LayoutGray: U64 := 4local const LayoutGrayAlpha: U64 := 5local const LayoutBgrx: U64 := 7local const LayoutGray16: U64 := 8local const LayoutGrayAlpha16: U64 := 9local const LayoutRgb16: U64 := 10local const LayoutRgba16: U64 := 11local const LayoutIndex1: U64 := 12local const LayoutIndex2: U64 := 13local const LayoutIndex4: U64 := 14local const LayoutIndex8: U64 := 15local const LayoutGray1: U64 := 16local const LayoutGray2: U64 := 17local const LayoutGray4: U64 := 18local const LayoutRgb565: U64 := 19local const LayoutRgb555: U64 := 20local const LayoutArgb1555: U64 := 21global function decode for Image(bytes: Bytes): ParseError ! Image {
match ImageFormat.detect(bytes) {
case ImageFormat.Png then {
return decodePng(bytes)!!
}
case ImageFormat.Jpeg then {
return decodeJpeg(bytes)!!
}
case ImageFormat.Gif then {
return decodeGifImage(bytes)!!
}
case ImageFormat.Bmp then {
return decodeBmp(bytes)!!
}
case ImageFormat.Qoi then {
return decodeQoi(bytes)!!
}
case ImageFormat.Tga then {
return decodeTga(bytes)!!
}
case ImageFormat.Pnm then {
return decodePnm(bytes)!!
}
case _ then {
error ParseError.new("not an image in a format this can read - PNG, JPEG, GIF, BMP, QOI, TGA or PNM")
}
}
}/// quality matters only to JPEG: 1 (smallest) to 100 (best)
global method encode for Image(self: Image, format: ImageFormat, quality: U8 := 90): Bytes {
match format {
case ImageFormat.Png then {
return self.encodePng()
}
case ImageFormat.Jpeg then {
return self.encodeJpeg(quality)
}
case ImageFormat.Gif then {
return self.encodeGif()
}
case ImageFormat.Bmp then {
return self.encodeBmp()
}
case ImageFormat.Qoi then {
return self.encodeQoi()
}
case ImageFormat.Tga then {
return self.encodeTga()
}
case _ then {
return self.encodePnm()
}
}
}/// lossless, 8 bits a sample: gray, gray with alpha, RGB or RGBA, whichever holds the pixels in least
global method encodePng for Image(self: Image): Bytes {
let (data, colorType) := pngData(self.rgba, self.columns, self.rows)
/// 8 bits a sample, deflate, the standard filters, not interlaced
let header := uintBytes(self.columns, 4, true) ++ uintBytes(self.rows, 4, true) ++ Bytes.fromArray([8, colorType as U8, 0, 0, 0])
return pngSignature() ++ pngChunk("IHDR", header) ++ pngChunk("IDAT", data) ++ pngChunk("IEND", Bytes.filled(0, 0))
} uses #builtin.pixelsPack
/// a 14-byte file header, a 108-byte BITMAPV4HEADER - its channel masks say the 4th byte is alpha -
/// then the rows bottom first, 4 bytes a pixel, so no row needs padding
global method encodeBmp for Image(self: Image): Bytes {
let pixels := #builtin.pixelsPack(self.rgba, self.columns, self.rows, LayoutBgra, self.columns * 4, true)
/// "BM", the file's size, 4 reserved bytes, where the pixels start
let fileHeader := "BM".encode() ++ uintBytes(122 + pixels.size(), 4) ++ uintBytes(0, 4) ++ uintBytes(122, 4)
/// its own size, width, height, 1 plane, 32 bits a pixel, BI_BITFIELDS, the optional fields left 0
let infoHeader := uintBytes(108, 4) ++ uintBytes(self.columns, 4) ++ uintBytes(self.rows, 4) ++ uintBytes(1, 2) ++ uintBytes(32, 2) ++ uintBytes(3, 4) ++ Bytes.filled(20, 0)
/// the red, green, blue and alpha masks, then sRGB and the unused endpoints and gammas
let masks := uintBytes(0x00FF0000, 4) ++ uintBytes(0x0000FF00, 4) ++ uintBytes(0x000000FF, 4) ++ uintBytes(0xFF000000, 4) ++ "BGRs".encode() ++ Bytes.filled(48, 0)
return fileHeader ++ infoHeader ++ masks ++ pixels
} uses #builtin.pixelsAnalyze, #builtin.qoiEncode
/// "qoif", the size, 3 channels (4 when any pixel isn't opaque), sRGB - then the chunks
global method encodeQoi for Image(self: Image): Bytes {
let channels: U8 := if (#builtin.pixelsAnalyze(self.rgba) as U64) band 1 != 0 then 4 else 3
let header := "qoif".encode() ++ uintBytes(self.columns, 4, true) ++ uintBytes(self.rows, 4, true) ++ Bytes.fromArray([channels, 0])
return header ++ #builtin.qoiEncode(self.rgba, self.columns * self.rows)
} uses #builtin.pixelsAnalyze, #builtin.pixelsPack, #builtin.rleEncode
/// run-length encoded true-color, top row first: 24-bit BGR, or 32-bit BGRA when any pixel isn't opaque
global method encodeTga for Image(self: Image): Bytes {
let alpha := (#builtin.pixelsAnalyze(self.rgba) as U64) band 1 != 0
let bpp: U64 := if alpha then 4 else 3
let packed := #builtin.pixelsPack(self.rgba, self.columns, self.rows, if alpha then LayoutBgra else LayoutBgr, self.columns * bpp, false)
/// no id, no color map, type 10 (RLE true-color), at 0, 0, the size, bits a pixel, top-left origin and the alpha bits
let header := Bytes.fromArray([0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0]) ++ uintBytes(self.columns, 2) ++ uintBytes(self.rows, 2) ++ Bytes.fromArray([(bpp * 8) as U8, if alpha then 0x28 else 0x20])
return header ++ #builtin.rleEncode(packed, self.columns * self.rows, bpp)
} uses #builtin.pixelsAnalyze, #builtin.pixelsPack
/// raw: PGM (P5) for gray pixels, PPM (P6) for color, PAM (P7) when any pixel isn't opaque
global method encodePnm for Image(self: Image): Bytes {
let found := #builtin.pixelsAnalyze(self.rgba) as U64
if found band 1 != 0 then {
let header := #format"P7\nWIDTH #{ self.columns }\nHEIGHT #{ self.rows }\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n"
return header.encode() ++ self.toRgba()
}
let gray := found band 2 == 0
let header := #format"P#{ if gray then 5 else 6 }\n#{ self.columns } #{ self.rows }\n255\n"
return header.encode() ++ #builtin.pixelsPack(self.rgba, self.columns, self.rows, if gray then LayoutGray else LayoutRgb, self.columns * (if gray then 1 else 3), false)
}/// at most 256 colors, dithered when there are more; alpha under 128 is transparent, the rest opaque
global method encodeGif for Image(self: Image): Bytes {
return gifHeader(self.columns, self.rows) ++ gifFrameBytes(self.rgba, self.columns, self.rows, 0, 0, 0, 0) ++ Bytes.fromArray([0x3B])
}/// the natural (row-major) index of the k-th coefficient in zigzag order
local const jpegZigzag: Array<U64> := [0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63]/// Annex K's quantization tables, natural order - luminance's, then chrominance's - what quality 50 is
local const jpegLuma: Array<U64> := [16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99]local const jpegChroma: Array<U64> := [17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99]/// Annex K's Huffman tables - DC and AC for luminance, then DC and AC for chrominance - each its 16
/// counts of codes by length, then its values
local const jpegHuffman: Array<U64> := [/// where each of the four Huffman tables starts in jpegHuffman, and where the last ends
local const jpegHuffmanStarts: Array<U64> := [0, 28, 206, 234, 412] uses #builtin.pixelsAnalyze, #builtin.jpegEncodeScan
global method encodeJpeg for Image(self: Image, quality: U8 := 90): Bytes {
let q := (quality as U64).max(1).min(100)
let luma := jpegQuantTable(jpegLuma, q)
let chroma := jpegQuantTable(jpegChroma, q)
let gray := (#builtin.pixelsAnalyze(self.rgba) as U64) band 2 == 0
let scan := #builtin.jpegEncodeScan(self.rgba, self.columns, self.rows, Bytes.fromArray(luma ++ chroma), Bytes.fromArray(jpegHuffman), true, gray)
/// JFIF 1.1, no units, 1:1 pixels, no thumbnail
let jfif := jpegSegment(0xE0, "JFIF".encode() ++ Bytes.fromArray([0, 1, 1, 0, 0, 1, 0, 1, 0, 0]))
/// each table its number, then its 64 values in zigzag order
let zigzagged := λ(table) Bytes.fromArray(jpegZigzag.map(λ(i) table[i]!!))
let quant := jpegSegment(0xDB, Bytes.fromArray([0]) ++ zigzagged(luma) ++ (if gray then Bytes.filled(0, 0) else Bytes.fromArray([1]) ++ zigzagged(chroma)))
/// 8-bit samples, the size, then each component: its id, its sampling (Y's 2x2 against 4:2:0's chroma), its table
let components := if gray then Bytes.fromArray([1, 1, 0x11, 0]) else Bytes.fromArray([3, 1, 0x22, 0, 2, 0x11, 1, 3, 0x11, 1])
let frame := jpegSegment(0xC0, Bytes.fromArray([8]) ++ uintBytes(self.rows, 2, true) ++ uintBytes(self.columns, 2, true) ++ components)
let table := λ(i, label) Bytes.fromArray([label]) ++ Bytes.fromArray(jpegHuffman.slice(jpegHuffmanStarts[i]!!..<jpegHuffmanStarts[i + 1]!!) !? [])
let huffman := jpegSegment(0xC4, table(0, 0x00) ++ table(1, 0x10) ++ (if gray then Bytes.filled(0, 0) else table(2, 0x01) ++ table(3, 0x11)))
/// each component's DC and AC tables, then the whole of the spectrum, no successive approximation
let start := jpegSegment(0xDA, (if gray then Bytes.fromArray([1, 1, 0x00]) else Bytes.fromArray([3, 1, 0x00, 2, 0x11, 3, 0x11])) ++ Bytes.fromArray([0, 63, 0]))
return Bytes.fromArray([0xFF, 0xD8]) ++ jfif ++ quant ++ frame ++ huffman ++ start ++ scan ++ Bytes.fromArray([0xFF, 0xD9])
}#equatable-for-imagemethod equals(self: Image, other: Image): Boolean {
return self.columns == other.columns and self.rows == other.rows and self.rgba == other.rgba
}#hashable-for-imagemethod hash(self: Image): U64 {
return self.rgba.hash()
}#tostring-for-image/// its size, not its pixels - "Image 640x480"
method toString(self: Image): String {
return #format"Image #{ self.columns }x#{ self.rows }"
}#indexable-u64-u64-color-for-imagemethod get(self: Image, key: (U64, U64)): Color? {
let (x, y) := key
return if x < self.columns and y < self.rows then self.at(x, y) else undefined
}method set(self: Image, key: (U64, U64), value: Color): Error ! Undefined {
let (x, y) := key
return self.setPixel(x, y, value)
}#default-for-imagefunction default(): Image {
return Image.new(0, 0)
}#clone-for-imagemethod clone(self: Image): Image {
return Image{columns := self.columns, rows := self.rows, rgba := self.toRgba()}
}#imageformat uses #builtin.bytesUint
/// what the bytes start with - TGA, which has no mark of its own, when its header makes sense
global function detect for ImageFormat(bytes: Bytes): ImageFormat? {
let size := bytes.size()
let byte := λ(at) (bytes[at] ?? 0) as U64
if bytes.startsWith(pngSignature()) then {
return ImageFormat.Png
}
if size >= 3 and byte(0) == 0xFF and byte(1) == 0xD8 and byte(2) == 0xFF then {
return ImageFormat.Jpeg
}
if bytes.startsWith("GIF8".encode()) then {
return ImageFormat.Gif
}
if bytes.startsWith("BM".encode()) then {
return ImageFormat.Bmp
}
if bytes.startsWith("qoif".encode()) then {
return ImageFormat.Qoi
}
/// "P1" to "P7", then whitespace
if size >= 3 and byte(0) == 0x50 and byte(1) >= 0x31 and byte(1) <= 0x37 and isPnmSpace(byte(2)) then {
return ImageFormat.Pnm
}
if size >= 18 and [0, 1].contains(byte(1)) and [1, 2, 3, 9, 10, 11].contains(byte(2)) and [8, 15, 16, 24, 32].contains(byte(16)) and #builtin.bytesUint(bytes, 12, 2, false) > 0.0 and #builtin.bytesUint(bytes, 14, 2, false) > 0.0 then {
return ImageFormat.Tga
}
return undefined
}#equatable-for-imageformatmethod equals(self: ImageFormat, other: ImageFormat): Boolean {
return self.index() == other.index()
}#hashable-for-imageformatmethod hash(self: ImageFormat): U64 {
return self.index().hash()
}#tostring-for-imageformatits name alone - "Png"
method toString(self: ImageFormat): String {
return self.variantName()
}#copy-for-imageformatNothing to write - a marker interface, or one another block already covers.
#videoLayout
global function new for Video(width: U64, height: U64, loops: U64 := 0): Video {
return Video{width, height, [], [], loops}
}global method width for Video(self: Video): U64 {
return self.columns
}global method height for Video(self: Video): U64 {
return self.rows
}global method frames for Video(self: Video): Array<Image> {
return self.images.map(λ(image) image)
}global method durations for Video(self: Video): Array<F64> {
return self.times.map(λ(t) t)
}global method frameCount for Video(self: Video): U64 {
return self.images.size()
}global method loops for Video(self: Video): U64 {
return self.plays
}/// every frame's duration, added up - in seconds
global method duration for Video(self: Video): F64 {
let total := 0.0
for t in self.times {
set total := total + t
}
return total
}/// frames a second, on average - 0 when there are none
global method fps for Video(self: Video): F64 {
let total := self.duration()
return if total > 0.0 then (self.images.size() as F64) / total else 0.0
}global method add for Video(self: Video, frame: Image, duration: F64 := 1.0 / 30.0): Error ! Undefined {
if frame.width() != self.columns or frame.height() != self.rows then {
error Error.new(#format"a #{ self.columns }x#{ self.rows } video can't take a #{ frame.width() }x#{ frame.height() } frame")
}
self.images.push(frame)
self.times.push(duration)
}/// the frame showing time seconds in - undefined past the end (or for no frames)
global method frameAt for Video(self: Video, time: F64): Image? {
let start := 0.0
for t, i in self.times {
if time >= start and time < start + t then {
return self.images[i]
}
set start := start + t
}
return undefined
}/// its frames in format - each its own duration in GIF and APNG, the average frame rate in Y4M and AVI
global method encode for Video(self: Video, format: VideoFormat, quality: U8 := 90): Bytes {
let fps := self.fps()
let encoder := VideoEncoder.new(format, self.columns, self.rows, if fps > 0.0 then fps else 30.0, quality, self.plays)
for frame, i in self.images {
encoder.add(frame, self.times[i]) !? undefined
}
return encoder.finish()
}global function decode for Video(bytes: Bytes): ParseError ! Video {
match VideoFormat.detect(bytes) {
case VideoFormat.Gif then {
return decodeGifVideo(bytes)!!
}
case VideoFormat.Apng then {
return decodeApng(bytes)!!
}
case VideoFormat.Y4m then {
return decodeY4m(bytes)!!
}
case VideoFormat.Avi then {
return decodeAvi(bytes)!!
}
case _ then {
error ParseError.new("not a video in a format this can read - GIF, APNG, Y4M or AVI")
}
}
}#tostring-for-videomethod toString(self: Video): String {
return #format"Video #{ self.columns }x#{ self.rows }, #{ self.images.size() } frames"
}#videoencoderLayout
global function new for VideoEncoder(format: VideoFormat, width: U64, height: U64, fps: F64 := 30.0, quality: U8 := 90, loops: U64 := 0): VideoEncoder {
return VideoEncoder{format, width, height, fps, quality, loops, [], []}
}global method frames for VideoEncoder(self: VideoEncoder): U64 {
return self.chunks.size()
} uses #builtin.pixelsAnalyze, #builtin.yuvEncode
/// a frame, shown for duration seconds (1 / fps when it's undefined) - only GIF and APNG keep a
/// duration for each frame
global method add for VideoEncoder(self: VideoEncoder, frame: Image, duration: F64? := undefined): Error ! Undefined {
if frame.width() != self.columns or frame.height() != self.rows then {
error Error.new(#format"a #{ self.columns }x#{ self.rows } video can't take a #{ frame.width() }x#{ frame.height() } frame")
}
let seconds := duration ?? 1.0 / self.rate
match self.format {
case VideoFormat.Gif then {
/// a frame with transparent pixels clears the last first, so it doesn't show through
let disposal: U64 := if (#builtin.pixelsAnalyze(frame.rgba) as U64) band 1 != 0 then 2 else 1
self.chunks.push(gifFrameBytes(frame.rgba, self.columns, self.rows, 0, 0, (seconds * 100.0).round() as U64, disposal))
}
case VideoFormat.Apng then {
let (data, _) := pngData(frame.rgba, self.columns, self.rows, true)
self.chunks.push(data)
self.delays.push(delayFraction(seconds))
}
case VideoFormat.Y4m then {
self.chunks.push("FRAME\n".encode() ++ #builtin.yuvEncode(frame.rgba, self.columns, self.rows, 0))
}
case _ then {
self.chunks.push(frame.encodeJpeg(self.quality))
}
}
}/// the whole file
global method finish for VideoEncoder(self: VideoEncoder): Bytes {
match self.format {
case VideoFormat.Gif then {
return gifHeader(self.columns, self.rows) ++ gifLoops(self.plays) ++ joined(self.chunks) ++ Bytes.fromArray([0x3B])
}
case VideoFormat.Apng then {
return apngBytes(self)
}
case VideoFormat.Y4m then {
let (num, den) := rateFraction(self.rate)
let header := #format"YUV4MPEG2 W#{ self.columns } H#{ self.rows } F#{ num }:#{ den } Ip A1:1 C420jpeg\n"
return header.encode() ++ joined(self.chunks)
}
case _ then {
return aviBytes(self)
}
}
}#videoformat/// what the bytes start with - a PNG counts as APNG, its one image a frame when it has no animation
global function detect for VideoFormat(bytes: Bytes): VideoFormat? {
if bytes.startsWith("GIF8".encode()) then {
return VideoFormat.Gif
}
if bytes.startsWith(Bytes.fromArray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) then {
return VideoFormat.Apng
}
if bytes.startsWith("YUV4MPEG2 ".encode()) then {
return VideoFormat.Y4m
}
if bytes.size() >= 12 and bytes.startsWith("RIFF".encode()) and bytes.slice(8..<12)!!.decode() == "AVI " then {
return VideoFormat.Avi
}
return undefined
}#equatable-for-videoformatmethod equals(self: VideoFormat, other: VideoFormat): Boolean {
return self.index() == other.index()
}#hashable-for-videoformatmethod hash(self: VideoFormat): U64 {
return self.index().hash()
}#tostring-for-videoformatits name alone - "Gif"
method toString(self: VideoFormat): String {
return self.variantName()
}#copy-for-videoformatNothing to write - a marker interface, or one another block already covers.
#soundLayout
/// how a format stores a sample (see media.wat's Samples)
local const EncodingUnsigned8: U64 := 0local const EncodingSigned8: U64 := 1local const EncodingInt16: U64 := 2local const EncodingInt16Big: U64 := 3local const EncodingInt24: U64 := 4local const EncodingInt24Big: U64 := 5local const EncodingInt32: U64 := 6local const EncodingInt32Big: U64 := 7local const EncodingFloat: U64 := 8local const EncodingFloatBig: U64 := 9local const EncodingDouble: U64 := 10local const EncodingDoubleBig: U64 := 11local const EncodingMuLaw: U64 := 12local const EncodingALaw: U64 := 13/// silence, duration seconds of it
global function new for Sound(sampleRate: U64 := 44100, channels: U64 := 2, duration: F64 := 0.0): Sound {
let frames := (duration * (sampleRate as F64)).round().max(0.0) as U64
return Sound{sampleRate, channels.max(1), Bytes.filled(frames * channels.max(1) * 4, 0)}
} uses #builtin.samplesFromArray
/// f(time, channel) for each sample - time in seconds from the start
global function generate for Sound(sampleRate: U64, channels: U64, duration: F64, f: Lambda<(F64, U64), F64>): Sound {
let frames := (duration * (sampleRate as F64)).round().max(0.0) as U64
let count := channels.max(1)
let values := Array.generate(frames * count, λ(i) f(((i ÷ count) as F64) / (sampleRate as F64), i % count))
return Sound{sampleRate, count, #builtin.samplesFromArray(values)}
} uses #builtin.samplesFromArray
global function fromSamples for Sound(sampleRate: U64, channels: U64, samples: Array<F64>): Error ! Sound {
if channels == 0 or samples.size() % channels != 0 then {
error Error.new(#format"#{ samples.size() } samples aren't a whole number of frames of #{ channels } channels")
}
return Sound{sampleRate, channels, #builtin.samplesFromArray(samples)}
}global method sampleRate for Sound(self: Sound): U64 {
return self.rate
}global method channels for Sound(self: Sound): U64 {
return self.count
}/// samples a channel
global method frames for Sound(self: Sound): U64 {
return self.data.size() ÷ 4 ÷ self.count
}/// in seconds
global method duration for Sound(self: Sound): F64 {
return (self.frames() as F64) / (self.rate as F64)
}global method sample for Sound(self: Sound, frame: U64, channel: U64): F64? {
if frame >= self.frames() or channel >= self.count then {
return undefined
}
let at := (frame * self.count + channel) * 4
return (#builtin.samplesToArray(self.data.slice(at..<at + 4)!!))[0]!!
} uses #builtin.samplesFromArray
global method setSample for Sound(self: Sound, frame: U64, channel: U64, value: F64): IndexError ! Undefined {
if frame >= self.frames() or channel >= self.count then {
error IndexError.new(#format"sample #{ frame } of channel #{ channel } is outside #{ self.frames() } of #{ self.count } channels")
}
self.data.copyFrom(#builtin.samplesFromArray([value]), (frame * self.count + channel) * 4)!!
}/// every sample, the channels interleaved
global method samples for Sound(self: Sound): Array<F64> {
return #builtin.samplesToArray(self.data)
} uses #builtin.samplesToArray, #builtin.samplesFromArray
/// one channel, alone
global method channel for Sound(self: Sound, index: U64): IndexError ! Sound {
if index >= self.count then {
error IndexError.new(#format"channel #{ index } of #{ self.count }")
}
let values := #builtin.samplesToArray(self.data)
return Sound{self.rate, 1, #builtin.samplesFromArray(Array.generate(self.frames(), λ(f) values[f * self.count + index]!!))}
}/// as count channels - all of them averaged to one, one copied to all, otherwise each channel its own
/// (the extra ones repeating the first)
global method withChannels for Sound(self: Sound, count: U64): Sound {
if count == self.count or count == 0 then {
return Sound{self.rate, self.count, self.data.slice(0..<self.data.size())!!}
}
return Sound{self.rate, count, #builtin.samplesRemix(self.data, self.count, count)}
}/// at sampleRate - band-limited, so nothing above the lower rate's Nyquist frequency aliases
global method resample for Sound(self: Sound, sampleRate: U64): Sound {
if sampleRate == self.rate or sampleRate == 0 then {
return Sound{self.rate, self.count, self.data.slice(0..<self.data.size())!!}
}
return Sound{sampleRate, self.count, #builtin.samplesResample(self.data, self.count, self.rate as F64, sampleRate as F64)}
}/// the part from start to end seconds - clipped to the sound
global method slice for Sound(self: Sound, start: F64, end: F64): Sound {
let frames := self.frames()
let from := ((start * (self.rate as F64)).round().max(0.0) as U64).min(frames)
let to := ((end * (self.rate as F64)).round().max(0.0) as U64).min(frames).max(from)
return Sound{self.rate, self.count, self.data.slice(from * self.count * 4..<to * self.count * 4)!!}
}/// other as self's rate and channels
local method matched for Sound(self: Sound, other: Sound): Sound {
return other.resample(self.rate).withChannels(self.count)
}/// other after self - at self's rate and channels
global method append for Sound(self: Sound, other: Sound): Sound {
return Sound{self.rate, self.count, self.data ++ self.matched(other).data}
} uses #builtin.samplesMix
/// other added in at seconds in, times gain - at self's rate and channels, as long as whichever ends later
global method mix for Sound(self: Sound, other: Sound, at: F64 := 0.0, gain: F64 := 1.0): Sound {
let added := self.matched(other)
let offset := (at * (self.rate as F64)).round().max(0.0) as U64
let frames := self.frames().max(offset + added.frames())
let out := Bytes.filled(frames * self.count * 4, 0)
out.copyFrom(self.data, 0)!!
#builtin.samplesMix(out, offset * self.count, added.data, gain)
return Sound{self.rate, self.count, out}
} uses #builtin.samplesGain
/// every sample times factor
global method gain for Sound(self: Sound, factor: F64): Sound {
return Sound{self.rate, self.count, #builtin.samplesGain(self.data, factor)}
} uses #builtin.samplesPeak
/// the largest magnitude of any sample - 1 is full scale
global method peak for Sound(self: Sound): F64 {
return #builtin.samplesPeak(self.data)
}/// scaled so its peak is level - as it is when it's silent
global method normalize for Sound(self: Sound, level: F64 := 1.0): Sound {
let peak := self.peak()
return if peak > 0.0 then self.gain(level / peak) else self.gain(1.0)
}global function decode for Sound(bytes: Bytes): ParseError ! Sound {
match AudioFormat.detect(bytes) {
case AudioFormat.Wav then {
return decodeWav(bytes)!!
}
case AudioFormat.Aiff then {
return decodeAiff(bytes)!!
}
case AudioFormat.Au then {
return decodeAu(bytes)!!
}
case AudioFormat.Flac then {
return decodeFlac(bytes)!!
}
case _ then {
error ParseError.new("not sound in a format this can read - WAV, AIFF, AU or FLAC")
}
}
}/// bits a sample: WAV 8, 16 or 24 (whole numbers) or 32 (floats); AIFF and AU 8, 16, 24 or 32 (whole
/// numbers); FLAC 8 to 24
global method encode for Sound(self: Sound, format: AudioFormat, bits: U8 := 16): Bytes {
match format {
case AudioFormat.Wav then {
return self.encodeWav(bits)
}
case AudioFormat.Aiff then {
return self.encodeAiff(bits)
}
case AudioFormat.Au then {
return self.encodeAu(bits)
}
case _ then {
return self.encodeFlac(bits)
}
}
} uses #builtin.pcmEncode
/// RIFF WAVE: a fmt chunk (PCM, or IEEE floats at 32 bits) and the data
global method encodeWav for Sound(self: Sound, bits: U8 := 16): Bytes {
let b := bitsOf(bits, [8, 16, 24, 32])
let data := #builtin.pcmEncode(self.data, if b == 8 then EncodingUnsigned8 else if b == 16 then EncodingInt16 else if b == 24 then EncodingInt24 else EncodingFloat)
let block := self.count * b ÷ 8
/// PCM (1) or IEEE floats (3), the channels, the rate, bytes a second, bytes a frame, bits a sample
let format := uintBytes(if b == 32 then 3 else 1, 2) ++ uintBytes(self.count, 2) ++ uintBytes(self.rate, 4) ++ uintBytes(self.rate * block, 4) ++ uintBytes(block, 2) ++ uintBytes(b, 2)
let body := "WAVE".encode() ++ riffChunk("fmt ", format) ++ riffChunk("data", data)
return "RIFF".encode() ++ uintBytes(body.size(), 4) ++ body
} uses #builtin.pcmEncode
/// big-endian chunks: FORM AIFF, COMM (the channels, frames, bits a sample and rate), SSND
global method encodeAiff for Sound(self: Sound, bits: U8 := 16): Bytes {
let b := bitsOf(bits, [8, 16, 24, 32])
let data := #builtin.pcmEncode(self.data, if b == 8 then EncodingSigned8 else if b == 16 then EncodingInt16Big else if b == 24 then EncodingInt24Big else EncodingInt32Big)
let common := uintBytes(self.count, 2, true) ++ uintBytes(self.frames(), 4, true) ++ uintBytes(b, 2, true) ++ extendedBytes(self.rate)
let chunk := λ(kind, body) kind.encode() ++ uintBytes(body.size(), 4, true) ++ body ++ (if body.size() % 2 == 1 then Bytes.filled(1, 0) else Bytes.filled(0, 0))
let body := "AIFF".encode() ++ chunk("COMM", common) ++ chunk("SSND", Bytes.filled(8, 0) ++ data)
return "FORM".encode() ++ uintBytes(body.size(), 4, true) ++ body
} uses #builtin.pcmEncode
/// ".snd", a 24-byte header, then 8- to 32-bit big-endian whole numbers
global method encodeAu for Sound(self: Sound, bits: U8 := 16): Bytes {
let b := bitsOf(bits, [8, 16, 24, 32])
let data := #builtin.pcmEncode(self.data, if b == 8 then EncodingSigned8 else if b == 16 then EncodingInt16Big else if b == 24 then EncodingInt24Big else EncodingInt32Big)
let header := ".snd".encode() ++ uintBytes(24, 4, true) ++ uintBytes(data.size(), 4, true) ++ uintBytes(b ÷ 8 + 1, 4, true) ++ uintBytes(self.rate, 4, true) ++ uintBytes(self.count, 4, true)
return header ++ data
} uses #builtin.flacEncode
/// "fLaC", STREAMINFO (blocks of 4096, no MD5), then the frames - lossless at bits (8 to 24) a sample
global method encodeFlac for Sound(self: Sound, bits: U8 := 16): Bytes {
let b := (bits as U64).max(8).min(24)
let encoded := #builtin.flacEncode(self.data, self.count, b, 4096)
let frames := encoded[0]!! as Bytes
let smallest := (encoded[1]!! as F64) as U64
let largest := (encoded[2]!! as F64) as U64
let total := self.frames()
/// the 64 bits after the frame sizes: rate, channels less 1, bits less 1, then the total's top 4 bits
let packed := uintBytes(self.rate shr 4, 2, true) ++ Bytes.fromArray([(((self.rate band 15) shl 4) bor ((self.count - 1) shl 1) bor ((b - 1) shr 4)) as U8, ((((b - 1) band 15) shl 4) bor ((total shr 32) band 15)) as U8]) ++ uintBytes(total band 0xFFFFFFFF, 4, true)
let info := uintBytes(4096, 2, true) ++ uintBytes(4096, 2, true) ++ uintBytes(smallest, 3, true) ++ uintBytes(largest, 3, true) ++ packed ++ Bytes.filled(16, 0)
/// the last metadata block (0x80), STREAMINFO (0), 34 bytes
return "fLaC".encode() ++ Bytes.fromArray([0x80, 0, 0, 34]) ++ info ++ frames
}#equatable-for-soundmethod equals(self: Sound, other: Sound): Boolean {
return self.rate == other.rate and self.count == other.count and self.data == other.data
}#tostring-for-soundmethod toString(self: Sound): String {
return #format"Sound #{ self.rate } Hz, #{ self.count } channels, #{ self.duration() } s"
}#audioformat/// what the bytes start with
global function detect for AudioFormat(bytes: Bytes): AudioFormat? {
let kind := if bytes.size() >= 12 then bytes.slice(8..<12)!!.decode() else ""
if bytes.startsWith("RIFF".encode()) and kind == "WAVE" then {
return AudioFormat.Wav
}
if bytes.startsWith("FORM".encode()) and (kind == "AIFF" or kind == "AIFC") then {
return AudioFormat.Aiff
}
if bytes.startsWith(".snd".encode()) then {
return AudioFormat.Au
}
if bytes.startsWith("fLaC".encode()) then {
return AudioFormat.Flac
}
return undefined
}#equatable-for-audioformatmethod equals(self: AudioFormat, other: AudioFormat): Boolean {
return self.index() == other.index()
}#hashable-for-audioformatmethod hash(self: AudioFormat): U64 {
return self.index().hash()
}#tostring-for-audioformatits name alone - "Wav"
method toString(self: AudioFormat): String {
return self.variantName()
}#copy-for-audioformatNothing to write - a marker interface, or one another block already covers.
#rectLayout
global function new for Rect(x: F64, y: F64, width: F64, height: F64): Rect {
return Rect{left := width < 0.0 ? x + width : x, top := height < 0.0 ? y + height : y, across := width.abs(), down := height.abs()}
}global function fromCorners for Rect(x1: F64, y1: F64, x2: F64, y2: F64): Rect {
return Rect.new(x1.min(x2), y1.min(y2), (x2 - x1).abs(), (y2 - y1).abs())
}global function around for Rect(points: Iterable<(F64, F64)>): EmptyError ! Rect {
let all := points.toArray()
let xs := all.map(λ((x, _)) x)
let ys := all.map(λ((_, y)) y)
let left := xs.min() !? (error EmptyError.new("no points to put a Rect around"))
let top := ys.min()!!
return Rect{left, top, across := xs.max()!! - left, down := ys.max()!! - top}
}global method x for Rect(self: Rect): F64 {
return self.left
}global method y for Rect(self: Rect): F64 {
return self.top
}global method width for Rect(self: Rect): F64 {
return self.across
}global method height for Rect(self: Rect): F64 {
return self.down
}global method right for Rect(self: Rect): F64 {
return self.left + self.across
}global method bottom for Rect(self: Rect): F64 {
return self.top + self.down
}global method center for Rect(self: Rect): (F64, F64) {
return (self.left + self.across / 2.0, self.top + self.down / 2.0)
}global method area for Rect(self: Rect): F64 {
return self.across * self.down
}global method isEmpty for Rect(self: Rect): Boolean {
return self.area() == 0.0
}global method containsPoint for Rect(self: Rect, x: F64, y: F64): Boolean {
return x >= self.left and x < self.right() and y >= self.top and y < self.bottom()
}global method containsRect for Rect(self: Rect, other: Rect): Boolean {
return other.left >= self.left and other.top >= self.top and other.right() <= self.right() and other.bottom() <= self.bottom()
}global method intersects for Rect(self: Rect, other: Rect): Boolean {
return self.left < other.right() and other.left < self.right() and self.top < other.bottom() and other.top < self.bottom()
}global method intersection for Rect(self: Rect, other: Rect): Rect? {
if not self.intersects(other) then {
return undefined
}
return Rect.fromCorners(self.left.max(other.left), self.top.max(other.top), self.right().min(other.right()), self.bottom().min(other.bottom()))
}global method union for Rect(self: Rect, other: Rect): Rect {
return Rect.fromCorners(self.left.min(other.left), self.top.min(other.top), self.right().max(other.right()), self.bottom().max(other.bottom()))
}global method expand for Rect(self: Rect, by: F64): Rect {
let (cx, cy) := self.center()
let width := (self.across + by * 2.0).max(0.0)
let height := (self.down + by * 2.0).max(0.0)
return Rect{left := cx - width / 2.0, top := cy - height / 2.0, across := width, down := height}
}global method translate for Rect(self: Rect, dx: F64, dy: F64): Rect {
return Rect{left := self.left + dx, top := self.top + dy, across := self.across, down := self.down}
}global method scale for Rect(self: Rect, factor: F64): Rect {
return Rect.new(self.left * factor, self.top * factor, self.across * factor, self.down * factor)
}#equatable-for-rectmethod equals(self: Rect, other: Rect): Boolean {
return self.left == other.left and self.top == other.top and self.across == other.across and self.down == other.down
}#hashable-for-rectmethod hash(self: Rect): U64 {
return (self.left, self.top, self.across, self.down).hash()
}#tostring-for-rect"Rect(10, 20, 30 × 40)"
method toString(self: Rect): String {
return #format"Rect(#{ self.left }, #{ self.top }, #{ self.across } × #{ self.down })"
}#copy-for-rectNothing to write - a marker interface, or one another block already covers.
#noiseLayout
/// Ken Perlin's improved noise - 0..<256 shuffled by the seed (a linear congruential generator), written
/// twice so a corner's index never wraps
global function new for Noise(seed: U64): Noise {
let order := (0..<256).map(λ(i) i as U64).toArray()
let state := seed
for i in (1..<256).reverse() {
set state := state *% 6364136223846793005 +% 1442695040888963407
order.swap(i, ((state shr 33) % (i + 1)) as U64)
}
return Noise{order := order ++ order}
}/// 6t^5 - 15t^4 + 10t^3 - flat at 0 and 1, so neighbouring cells meet smoothly
local function fade for Noise(t: F64): F64 {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}local function mix for Noise(a: F64, b: F64, t: F64): F64 {
return a + t * (b - a)
}/// (x, y, z) dotted with one of 12 edge directions, picked by hash's low 4 bits
local function gradient for Noise(hash: U64, x: F64, y: F64, z: F64): F64 {
let h := hash % 16
let u := h < 8 ? x : y
let v := h < 4 ? y : h == 12 | 14 ? x : z
return (h % 2 == 0 ? u : -u) + (h // 2 % 2 == 0 ? v : -v)
}global method at for Noise(self: Noise, x: F64, y: F64 := 0.0, z: F64 := 0.0): F64 {
let p := self.order
let (fx, fy, fz) := (x.floor(), y.floor(), z.floor())
let (cx, cy, cz) := ((fx as S64).modulo(256) as U64, (fy as S64).modulo(256) as U64, (fz as S64).modulo(256) as U64)
let (px, py, pz) := (x - fx, y - fy, z - fz)
let (u, v, w) := (fade(px), fade(py), fade(pz))
let a := p[cx]!! + cy
let b := p[cx + 1]!! + cy
let (aa, ab, ba, bb) := (p[a]!! + cz, p[a + 1]!! + cz, p[b]!! + cz, p[b + 1]!! + cz)
let layer := λ(dz: U64) mix(
mix(gradient(p[aa + dz]!!, px, py, pz - dz as F64), gradient(p[ba + dz]!!, px - 1.0, py, pz - dz as F64), u),
mix(gradient(p[ab + dz]!!, px, py - 1.0, pz - dz as F64), gradient(p[bb + dz]!!, px - 1.0, py - 1.0, pz - dz as F64), u),
v)
return mix(layer(0), layer(1), w)
}global method fractal for Noise(self: Noise, x: F64, y: F64 := 0.0, z: F64 := 0.0, octaves: U64 := 4, persistence: F64 := 0.5): F64 {
let layers := (0..<octaves.max(1)).map(λ(i) (persistence ^ i as F64, 2.0 ^ i as F64))
let total := layers.map(λ((strength, frequency)) self.at(x * frequency, y * frequency, z * frequency) * strength).sum()
return total / layers.map(λ((strength, _)) strength).sum()
}#easing/// easings.net's curves
global method apply for Easing(self: Easing, t: F64): F64 {
let x := t.clamp(0.0..=1.0)
let pi := F64.pi()
let back := 1.70158
match self {
case Easing.Linear then { return x }
case Easing.InQuad then { return x * x }
case Easing.OutQuad then { return 1.0 - (1.0 - x) ^ 2.0 }
case Easing.InOutQuad then { return x < 0.5 ? 2.0 * x * x : 1.0 - (-2.0 * x + 2.0) ^ 2.0 / 2.0 }
case Easing.InCubic then { return x ^ 3.0 }
case Easing.OutCubic then { return 1.0 - (1.0 - x) ^ 3.0 }
case Easing.InOutCubic then { return x < 0.5 ? 4.0 * x ^ 3.0 : 1.0 - (-2.0 * x + 2.0) ^ 3.0 / 2.0 }
case Easing.InSine then { return 1.0 - (x * pi / 2.0).cos() }
case Easing.OutSine then { return (x * pi / 2.0).sin() }
case Easing.InOutSine then { return -((pi * x).cos() - 1.0) / 2.0 }
case Easing.InExpo then { return x == 0.0 ? 0.0 : 2.0 ^ (10.0 * x - 10.0) }
case Easing.OutExpo then { return x == 1.0 ? 1.0 : 1.0 - 2.0 ^ (-10.0 * x) }
case Easing.InOutExpo then {
return x == 0.0 or x == 1.0 ? x : x < 0.5 ? 2.0 ^ (20.0 * x - 10.0) / 2.0 : (2.0 - 2.0 ^ (-20.0 * x + 10.0)) / 2.0
}
case Easing.InBack then { return (back + 1.0) * x ^ 3.0 - back * x * x }
case Easing.OutBack then { return 1.0 + (back + 1.0) * (x - 1.0) ^ 3.0 + back * (x - 1.0) ^ 2.0 }
case Easing.OutElastic then {
return x == 0.0 or x == 1.0 ? x : 2.0 ^ (-10.0 * x) * ((x * 10.0 - 0.75) * (2.0 * pi / 3.0)).sin() + 1.0
}
case Easing.OutBounce then {
let (n, d) := (7.5625, 2.75)
for (edge, shift, add) in [(1.0 / d, 0.0, 0.0), (2.0 / d, 1.5 / d, 0.75), (2.5 / d, 2.25 / d, 0.9375)] if x < edge {
return n * (x - shift) ^ 2.0 + add
}
return n * (x - 2.625 / d) ^ 2.0 + 0.984375
}
}
}global method between for Easing(self: Easing, from: F64, to: F64, t: F64): F64 {
return from + (to - from) * self.apply(t)
}#equatable-for-easingmethod equals(self: Easing, other: Easing): Boolean {
return self.index() == other.index()
}#hashable-for-easingmethod hash(self: Easing): U64 {
return self.index().hash()
}#tostring-for-easingits name alone - "InOutCubic"
method toString(self: Easing): String {
return self.variantName()
}#copy-for-easingNothing to write - a marker interface, or one another block already covers.
#gpuType: Steppable and Comparable
Layout
/// the host's WebGPU - a JavaScript host provides it, a web page's own; it asked for the device while the
/// program loaded. Each object it makes is the host's, held as a JsValue. A refused call - a descriptor
/// WebGPU won't take - throws, which comes back as an Error
@js("host:gpu", "device")
local function hostDevice for Gpu(): JsValue@js("host:gpu", "canvas")
local function hostCanvas for Gpu(device: JsValue, id: String): GpuCanvas@js("host:gpu", "buffer")
local function hostBuffer for Gpu(device: JsValue, size: U64, usage: Array<GpuBufferUsage>): JsValue@js("host:gpu", "write")
local function hostWrite for Gpu(device: JsValue, buffer: JsValue, data: Bytes, offset: U64): Undefined@js("host:gpu", "writeFloats")
local function hostWriteFloats for Gpu(device: JsValue, buffer: JsValue, values: Array<F64>, offset: U64): Undefined@js("host:gpu", "writeIndices")
local function hostWriteIndices for Gpu(device: JsValue, buffer: JsValue, values: Array<U64>, offset: U64): Undefined@js("host:gpu", "shader")
local function hostShader for Gpu(device: JsValue, code: String): JsValue@js("host:gpu", "texture")
local function hostTexture for Gpu(device: JsValue, width: U64, height: U64, format: GpuTextureFormat): JsValue@js("host:gpu", "pipeline")
local function hostPipeline for Gpu(device: JsValue, options: GpuPipelineOptions): JsValue@js("host:gpu", "computePipeline")
local function hostComputePipeline for Gpu(device: JsValue, shader: JsValue, entry: String): JsValue@js("host:gpu", "bindGroup")
local function hostBindGroup for Gpu(device: JsValue, pipeline: JsValue, group: U32, buffers: Array<JsValue>): JsValue/// each pass as its kind, then what that kind holds: "render", the canvas's context, the clear color, the
/// depth texture, the commands - or "compute", the commands
@js("host:gpu", "submit")
local function hostSubmit for Gpu(device: JsValue, passes: Array<Array<Unknown>>): Undefined/// the buffer's bytes, once the GPU has run what was submitted before. Mapping a buffer is a Promise in
/// WebGPU: a host that can wait for it (Bun and Node, whose device is on a thread of its own) gives the
/// bytes now, as the answer's body with number 0; a web page can't, and gives a negative number of its
/// own - the bytes come later, through Builtin.httpAnswered, as a request's answer does
@js("host:gpu", "read")
local function hostRead for Gpu(device: JsValue, buffer: JsValue, size: U64): HostHttpAnswer/// a texture drawn into off any page, RGBA - shaped like a page canvas's
@js("host:gpu", "offscreen")
local function hostOffscreen for Gpu(device: JsValue, width: U64, height: U64): GpuCanvas/// what's drawn into a target, copied out as RGBA rows - as hostRead reads a buffer: now, or later on a page
@js("host:gpu", "readTexture")
local function hostReadTexture for Gpu(device: JsValue, context: JsValue, width: U64, height: U64, format: GpuTextureFormat): HostHttpAnswer/// bytes as the F32s they hold
@js("host:gpu", "floats")
local function hostFloats for Gpu(data: Bytes): Array<F64>@js("host:gpu", "animate")
local function hostAnimate for Gpu(frame: String): Undefined/// what the host refused, as an IOError
local function io for Gpu<T>(result: Error ! T): IOError ! T {
match result {
case Error(message) then { error IOError.new(message.toString()) }
case value then { return value }
}
}/// what the host made - a refusal is a mistake in the program, so a panic with WebGPU's message
local function made for Gpu<T>(result: Error ! T): T {
match result {
case Error(message) then { return abort(Error.new("WebGPU: " ++ message.toString())) }
case value then { return value }
}
}global function request for Gpu(): IOError ! Gpu {
return Self{device := Self.io(Self.hostDevice())!!}
}global method canvas for Gpu(self: Gpu, id: String): IOError ! GpuCanvas {
return Self.io(Self.hostCanvas(self.device, id))!!
}global method buffer for Gpu(self: Gpu, size: U64, usage: Array<GpuBufferUsage>): GpuBuffer {
return GpuBuffer{handle := Self.made(Self.hostBuffer(self.device, size, usage)), size}
}global method floatBuffer for Gpu(self: Gpu, values: Array<F64>, usage: Array<GpuBufferUsage>): GpuBuffer {
let buffer := self.buffer(values.size() * 4, usage)
self.writeFloats(buffer, values)
return buffer
}global method indexBuffer for Gpu(self: Gpu, indices: Array<U64>): GpuBuffer {
let buffer := self.buffer(indices.size() * 2, [GpuBufferUsage.Index])
Self.made(Self.hostWriteIndices(self.device, buffer.handle, indices, 0))
return buffer
}global method write for Gpu(self: Gpu, buffer: GpuBuffer, data: Bytes, offset: U64 := 0): Undefined {
Self.made(Self.hostWrite(self.device, buffer.handle, data, offset))
}global method writeFloats for Gpu(self: Gpu, buffer: GpuBuffer, values: Array<F64>, offset: U64 := 0): Undefined {
Self.made(Self.hostWriteFloats(self.device, buffer.handle, values, offset))
}global method writeMatrix for Gpu(self: Gpu, buffer: GpuBuffer, matrix: Matrix<F64, 4, 4>, offset: U64 := 0): Undefined {
self.writeFloats(buffer, matrix.columns().flatMap(λ(column) column.toArray()), offset)
}global method shader for Gpu(self: Gpu, code: Wgsl | String): GpuShader {
return GpuShader{handle := Self.made(Self.hostShader(self.device, code.toString()))}
}global method depthTexture for Gpu(self: Gpu, canvas: GpuCanvas): GpuTexture {
let format := GpuTextureFormat.Depth24Plus
return GpuTexture{handle := Self.made(Self.hostTexture(self.device, canvas.width, canvas.height, format)), format}
}global method pipeline for Gpu(self: Gpu, options: GpuPipelineOptions): GpuPipeline {
return GpuPipeline{handle := Self.made(Self.hostPipeline(self.device, options))}
}global method computePipeline for Gpu(self: Gpu, shader: GpuShader, entry: String := "computeMain"): GpuComputePipeline {
return GpuComputePipeline{handle := Self.made(Self.hostComputePipeline(self.device, shader.handle, entry))}
}global method bindGroup for Gpu(self: Gpu, pipeline: GpuPipeline | GpuComputePipeline, group: U32, buffers: Array<GpuBuffer>): GpuBindGroup {
let handle := match pipeline {
case GpuComputePipeline as compute then compute.handle
case GpuPipeline as render then render.handle
}
return GpuBindGroup{handle := Self.made(Self.hostBindGroup(self.device, handle, group, buffers.map(λ(buffer) buffer.handle)))}
}global method submit for Gpu(self: Gpu, passes: Array<GpuRenderPass | GpuComputePass>): Undefined {
let described: Array<Array<Unknown>> := passes.map(λ(pass) match pass {
case GpuComputePass as compute then ["compute", compute.commands]
case GpuRenderPass as render then ["render", render.target, render.clear, render.depth, render.commands]
})
Self.made(Self.hostSubmit(self.device, described))
}global method read for Gpu(self: Gpu, buffer: GpuBuffer): Promise<IOError ! Bytes> {
match Self.hostRead(self.device, buffer.handle, buffer.size) {
case Error(message) then { return Promise.resolved(Self.readFailed(message.toString())) }
case answer then {
if answer.number == 0.0 then {
return Promise.resolved(Self.bytesOf(answer))
}
return Builtin.hostAnswerLater(answer.number).map(λ(later) Self.bytesOf(later))
}
}
}global method readFloats for Gpu(self: Gpu, buffer: GpuBuffer): Promise<IOError ! Array<F64>> {
return self.read(buffer).map(λ(bytes) Self.hostFloats(bytes!!))
}global method offscreen for Gpu(self: Gpu, width: U64, height: U64): GpuCanvas {
return Self.made(Self.hostOffscreen(self.device, width, height)) as GpuCanvas
}/// a target's pixels as its Image - read as read reads a buffer
global method readImage for Gpu(self: Gpu, target: GpuCanvas): Promise<IOError ! Image> {
let image := λ(bytes) Image.fromRgba(target.width, target.height, bytes!!) !? Image.new(target.width, target.height)
match Self.hostReadTexture(self.device, target.context, target.width, target.height, target.format) {
case Error(message) then { return Promise.resolved(image(Self.readFailed(message.toString()))) }
case answer then {
if answer.number == 0.0 then {
return Promise.resolved(image(Self.bytesOf(answer)))
}
return Builtin.hostAnswerLater(answer.number).map(λ(later) image(Self.bytesOf(later)))
}
}
}/// what a read gave: the bytes, or why there are none
local function bytesOf for Gpu(answer: HostHttpAnswer): IOError ! Bytes {
match answer.failure {
case String as failure then { return Self.readFailed(failure) }
case _ then { return answer.body as Bytes }
}
}local function readFailed for Gpu(message: String): IOError ! Bytes {
error IOError.new("the GPU buffer couldn't be read: " ++ message)
}global method animate for Gpu(self: Gpu, frame: String): Undefined {
Self.made(Self.hostAnimate(frame))
}#tostring-for-gpumethod toString(self: Gpu): String {
return "Gpu"
}#gpucanvasLayout
No members of its own.
#tostring-for-gpucanvas/// "GpuCanvas 640x480"
method toString(self: GpuCanvas): String {
return "GpuCanvas " ++ self.width.toString() ++ "x" ++ self.height.toString()
}#gpubufferLayout
No members of its own.
#tostring-for-gpubuffer/// "GpuBuffer of 64 bytes"
method toString(self: GpuBuffer): String {
return "GpuBuffer of " ++ self.size.toString() ++ " bytes"
}#gputextureLayout
No members of its own.
#tostring-for-gputexture/// "GpuTexture Depth24Plus"
method toString(self: GpuTexture): String {
return "GpuTexture " ++ self.format.toString()
}#gpushaderLayout
No members of its own.
#tostring-for-gpushadermethod toString(self: GpuShader): String {
return "GpuShader"
}#gpupipelineLayout
No members of its own.
#tostring-for-gpupipelinemethod toString(self: GpuPipeline): String {
return "GpuPipeline"
}#gpubindgroupLayout
No members of its own.
#tostring-for-gpubindgroupmethod toString(self: GpuBindGroup): String {
return "GpuBindGroup"
}#gpuvertexattributeLayout
No members of its own.
#equatable-for-gpuvertexattributemethod equals(self: GpuVertexAttribute, other: GpuVertexAttribute): Boolean {
return self.format == other.format and self.offset == other.offset and self.location == other.location
}#tostring-for-gpuvertexattribute/// "Float32x3 at 12, @location(1)"
method toString(self: GpuVertexAttribute): String {
return self.format.toString() ++ " at " ++ self.offset.toString() ++ ", @location(" ++ self.location.toString() ++ ")"
}#clone-for-gpuvertexattributemethod clone(self: GpuVertexAttribute): GpuVertexAttribute {
return GpuVertexAttribute{self.format, self.offset, self.location}
}#gpuvertexlayoutLayout
No members of its own.
#equatable-for-gpuvertexlayoutmethod equals(self: GpuVertexLayout, other: GpuVertexLayout): Boolean {
return self.stride == other.stride and self.attributes == other.attributes and self.step == other.step
}#tostring-for-gpuvertexlayout/// "24 bytes a Vertex: Float32x3 at 0, @location(0), Float32x3 at 12, @location(1)"
method toString(self: GpuVertexLayout): String {
return self.stride.toString() ++ " bytes a " ++ self.step.toString() ++ ": " ++ self.attributes.map(λ(a) a.toString()).join(", ")
}#clone-for-gpuvertexlayoutmethod clone(self: GpuVertexLayout): GpuVertexLayout {
return GpuVertexLayout{self.stride, self.attributes.map(λ(a) a.clone()), self.step}
}#gpupipelineoptionsLayout
No members of its own.
#tostring-for-gpupipelineoptions/// "GpuPipelineOptions vertexMain/fragmentMain, TriangleList"
method toString(self: GpuPipelineOptions): String {
return "GpuPipelineOptions " ++ self.vertexEntry ++ "/" ++ self.fragmentEntry ++ ", " ++ self.topology.toString()
}#clone-for-gpupipelineoptionsmethod clone(self: GpuPipelineOptions): GpuPipelineOptions {
return GpuPipelineOptions{self.shader, self.target, self.buffers.map(λ(b) b.clone()), self.vertexEntry, self.fragmentEntry, self.topology, self.cullMode, self.depth}
}#gpurenderpassLayout
global function new for GpuRenderPass(target: GpuCanvas, clear: Color := Color.rgb(0, 0, 0), depth: GpuTexture? := undefined): GpuRenderPass {
let channel := λ(value: U8) (value as F64) / 255.0
return Self{target := target.context, clear := [channel(clear.red()), channel(clear.green()), channel(clear.blue()), channel(clear.alpha())], depth := depth?.handle, commands := []}
}global method setPipeline for GpuRenderPass(self: GpuRenderPass, pipeline: GpuPipeline): Undefined {
self.commands.push(["pipeline", pipeline.handle])
}global method setVertexBuffer for GpuRenderPass(self: GpuRenderPass, slot: U32, buffer: GpuBuffer): Undefined {
self.commands.push(["vertexBuffer", slot, buffer.handle])
}global method setIndexBuffer for GpuRenderPass(self: GpuRenderPass, buffer: GpuBuffer, format: GpuIndexFormat := GpuIndexFormat.Uint16): Undefined {
self.commands.push(["indexBuffer", buffer.handle, format])
}global method setBindGroup for GpuRenderPass(self: GpuRenderPass, index: U32, group: GpuBindGroup): Undefined {
self.commands.push(["bindGroup", index, group.handle])
}global method draw for GpuRenderPass(self: GpuRenderPass, vertices: U32, instances: U32 := 1, firstVertex: U32 := 0, firstInstance: U32 := 0): Undefined {
self.commands.push(["draw", vertices, instances, firstVertex, firstInstance])
}global method drawIndexed for GpuRenderPass(self: GpuRenderPass, indices: U32, instances: U32 := 1, firstIndex: U32 := 0): Undefined {
self.commands.push(["drawIndexed", indices, instances, firstIndex])
}#tostring-for-gpurenderpass/// "GpuRenderPass with 3 commands"
method toString(self: GpuRenderPass): String {
let count := self.commands.size()
return "GpuRenderPass with " ++ count.toString() ++ (count == 1 ? " command" : " commands")
}#gpucomputepipelineLayout
No members of its own.
#tostring-for-gpucomputepipelinemethod toString(self: GpuComputePipeline): String {
return "GpuComputePipeline"
}#gpucomputepassLayout
global function new for GpuComputePass(): GpuComputePass {
return Self{commands := []}
}global method setPipeline for GpuComputePass(self: GpuComputePass, pipeline: GpuComputePipeline): Undefined {
self.commands.push(["pipeline", pipeline.handle])
}global method setBindGroup for GpuComputePass(self: GpuComputePass, index: U32, group: GpuBindGroup): Undefined {
self.commands.push(["bindGroup", index, group.handle])
}global method dispatch for GpuComputePass(self: GpuComputePass, x: U32, y: U32 := 1, z: U32 := 1): Undefined {
self.commands.push(["dispatch", x, y, z])
}#tostring-for-gpucomputepass/// "GpuComputePass with 3 commands"
method toString(self: GpuComputePass): String {
let count := self.commands.size()
return "GpuComputePass with " ++ count.toString() ++ (count == 1 ? " command" : " commands")
}#jsvalueLayout
/// the host's "host:dom" - a JavaScript host provides it. What a call gives is an object held as a
/// JsValue (an Array and a plain object too), null and undefined as undefined, anything else as its
/// Refingere value; what it throws comes back as an Error. What JavaScript does later - an event, a
/// promise settling, a callback called - comes back to the task #runtime.listen registered for it
@js("host:dom", "global")
local function hostGlobal for JsValue(): JsValue@js("host:dom", "window")
local function hostWindow for JsValue(): JsValue@js("host:dom", "document")
local function hostDocument for JsValue(): JsValue@js("host:dom", "get")
local function hostGet for JsValue(target: JsValue, name: String): JsValue?@js("host:dom", "set")
local function hostSet for JsValue(target: JsValue, name: String, value: Unknown): Undefined@js("host:dom", "call")
local function hostCall for JsValue(target: JsValue, name: String, arguments: Array<Unknown>): JsValue?@js("host:dom", "invoke")
local function hostInvoke for JsValue(target: JsValue, arguments: Array<Unknown>): JsValue?@js("host:dom", "construct")
local function hostConstruct for JsValue(target: JsValue, arguments: Array<Unknown>): JsValue?@js("host:dom", "typeOf")
local function hostTypeOf for JsValue(value: JsValue): String/// anything JavaScript can iterate, as an Array
@js("host:dom", "list")
local function hostList for JsValue(value: JsValue): Array<JsValue?>/// the value as plain data, all the way down
@js("host:dom", "data")
local function hostData for JsValue(value: JsValue): Unknown@js("host:dom", "isElement")
local function hostIsElement for JsValue(value: JsValue): Boolean/// a JavaScript function calling the program back - the task listened under number, with its arguments
@js("host:dom", "callback")
local function hostCallback for JsValue(number: F64): JsValue/// the task listened under number called with [true, value] once the JavaScript promise settles,
/// [false, message] when it's rejected
@js("host:dom", "awaiting")
local function hostAwaiting for JsValue(value: JsValue, number: F64): Undefined/// the task listened under number called with each kind event target hears - the event held only
/// while it runs
@js("host:dom", "listen")
local function hostListen for JsValue(target: JsValue, kind: String, number: F64, capture: Boolean, once: Boolean): Undefined@js("host:dom", "unlisten")
local function hostUnlisten for JsValue(number: F64): Undefined/// what a refused call threw, as a panic - a mistake in the program, not something it handles
local function made for JsValue<T>(result: Error ! T): T {
match result {
case Error(message) then { return abort(Error.new("JavaScript: " ++ message.toString())) }
case value then { return value }
}
}/// a value as JavaScript gets it - an Element as its element, a Canvas2d as its context, Html as its markup
local function argument for JsValue(value: Unknown): Unknown {
return match value {
case Element as element then element.node
case Canvas2d as canvas then canvas.context
case Page as page then page.window
case Html as html then html.markup
case other then other
}
}global function global for JsValue(): IOError ! JsValue {
match Self.hostGlobal() {
case Error(message) then { error IOError.new(message.toString()) }
case value then { return value }
}
} uses #runtime.listen
global function callback for JsValue(task: Lambda<Array<Unknown>, Unknown>): JsValue {
return Self.made(Self.hostCallback(#runtime.listen(λ(arguments) task(arguments as Array<Unknown>))))
}global method get for JsValue(self: JsValue, name: String): Unknown {
return Self.made(Self.hostGet(self, name))
}global method set for JsValue(self: JsValue, name: String, value: Unknown): Undefined {
Self.made(Self.hostSet(self, name, Self.argument(value)))
}global method call for JsValue(self: JsValue, name: String, arguments: ..Unknown): Error ! Unknown {
return Self.hostCall(self, name, arguments.map(λ(value) Self.argument(value)))
}global method invoke for JsValue(self: JsValue, arguments: ..Unknown): Error ! Unknown {
return Self.hostInvoke(self, arguments.map(λ(value) Self.argument(value)))
}global method construct for JsValue(self: JsValue, arguments: ..Unknown): Error ! Unknown {
return Self.hostConstruct(self, arguments.map(λ(value) Self.argument(value)))
}global method items for JsValue(self: JsValue): Array<Unknown> {
return Self.made(Self.hostList(self))
}global method toData for JsValue(self: JsValue): Unknown {
return Self.made(Self.hostData(self))
}global method typeOf for JsValue(self: JsValue): String {
return Self.made(Self.hostTypeOf(self))
} uses #runtime.listen
global method toPromise for JsValue(self: JsValue): Promise<Error ! Unknown> {
let (done, settle) := Promise<Error ! Unknown>.withResolver()
let number := #runtime.listen(λ(outcome) {
let settled := outcome as Array<Unknown>
settle(settled[0] == true ? settled[1] : Error.new(settled[1].toString()))
})
Self.made(Self.hostAwaiting(self, number))
return done
} uses #runtime.listen
global method on for JsValue(self: JsValue, kind: String, handler: Lambda<DomEvent, Undefined>): Listener {
let number := #runtime.listen(λ(event) handler(DomEvent.of(event as JsValue)))
Self.made(Self.hostListen(self, kind, number, false, false))
return Listener{number, kind, active := true}
} uses #runtime.listen
global method events for JsValue(self: JsValue, kind: String, limit: U64 := 1000): EventQueue {
let queue := EventQueue{number := 0.0, kind, limit, waiting := Queue.new(), takers := Queue.new(), closed := false}
set queue.number := #runtime.listen(λ(event) queue.add(DomEvent.of(event as JsValue)))
Self.made(Self.hostListen(self, kind, queue.number, false, false))
return queue
}#tostring-for-jsvalue"JsValue #3"
method toString(self: JsValue): String {
return "JsValue #" ++ self.handle.toString()
}#pageLayout
global function current for Page(): IOError ! Page {
match JsValue.hostWindow() {
case Error(message) then { error IOError.new(message.toString()) }
case window then { return Self{window, document := JsValue.made(JsValue.hostDocument())} }
}
}local function location for Page(self: Page): JsValue {
return self.window.get("location") as JsValue
}local function history for Page(self: Page): JsValue {
return self.window.get("history") as JsValue
}global method body for Page(self: Page): Element {
return Element{node := self.document.get("body") as JsValue}
}global method head for Page(self: Page): Element {
return Element{node := self.document.get("head") as JsValue}
}global method root for Page(self: Page): Element {
return Element{node := self.document.get("documentElement") as JsValue}
}global method find for Page(self: Page, selector: String): Element? {
return Element.of(JsValue.made(self.document.call("querySelector", selector)))
}global method findAll for Page(self: Page, selector: String): Array<Element> {
return Element.allOf(JsValue.made(self.document.call("querySelectorAll", selector)))
}global method byId for Page(self: Page, id: String): Element? {
return Element.of(JsValue.made(self.document.call("getElementById", id)))
}global method create for Page(self: Page, tag: String): Element {
return Element{node := JsValue.made(self.document.call("createElement", tag)) as JsValue}
}/// parsed in a <template>, so nothing in it runs or loads until it's on the page
global method fromHtml for Page(self: Page, markup: Html): Array<Element> {
let template := JsValue.made(self.document.call("createElement", "template")) as JsValue
template.set("innerHTML", markup.markup)
return Element.allOf((template.get("content") as JsValue).get("children"))
}global method focused for Page(self: Page): Element? {
return Element.of(self.document.get("activeElement"))
}global method addStyle for Page(self: Page, css: Stylesheet | String): Element {
let style := self.create("style")
style.setText(css.toString())
self.head().append(style)
return style
}global method title for Page(self: Page): String {
return self.document.get("title").toString()
}global method setTitle for Page(self: Page, title: String): Undefined {
self.document.set("title", title)
}global method url for Page(self: Page): Url {
return Url.parse(self.location().get("href").toString())!!
}global method reload for Page(self: Page): Undefined {
JsValue.made(self.location().call("reload"))
}global method pushHistory for Page(self: Page, url: String): Undefined {
JsValue.made(self.history().call("pushState", undefined, "", url))
}global method replaceHistory for Page(self: Page, url: String): Undefined {
JsValue.made(self.history().call("replaceState", undefined, "", url))
}global method back for Page(self: Page): Undefined {
JsValue.made(self.history().call("back"))
}global method forward for Page(self: Page): Undefined {
JsValue.made(self.history().call("forward"))
}global method width for Page(self: Page): F64 {
return self.window.get("innerWidth") as F64
}global method height for Page(self: Page): F64 {
return self.window.get("innerHeight") as F64
}global method pixelRatio for Page(self: Page): F64 {
return self.window.get("devicePixelRatio") as F64? ?? 1.0
}global method scrollX for Page(self: Page): F64 {
return self.window.get("scrollX") as F64
}global method scrollY for Page(self: Page): F64 {
return self.window.get("scrollY") as F64
}global method scrollTo for Page(self: Page, x: F64, y: F64): Undefined {
JsValue.made(self.window.call("scrollTo", x, y))
}global method alert for Page(self: Page, message: String): Undefined {
JsValue.made(self.window.call("alert", message))
}global method confirm for Page(self: Page, message: String): Boolean {
return JsValue.made(self.window.call("confirm", message)) == true
}global method prompt for Page(self: Page, message: String, value: String := ""): String? {
return JsValue.made(self.window.call("prompt", message, value)) as String?
}global method localStorage for Page(self: Page): WebStorage {
return WebStorage{storage := self.window.get("localStorage") as JsValue}
}global method sessionStorage for Page(self: Page): WebStorage {
return WebStorage{storage := self.window.get("sessionStorage") as JsValue}
}global method on for Page(self: Page, kind: String, handler: Lambda<DomEvent, Undefined>): Listener {
return self.window.on(kind, handler)
}global method events for Page(self: Page, kind: String, limit: U64 := 1000): EventQueue {
return self.window.events(kind, limit)
}global method window for Page(self: Page): JsValue {
return self.window
}global method document for Page(self: Page): JsValue {
return self.document
}#tostring-for-page"Page https://example.com/"
method toString(self: Page): String {
return "Page " ++ self.location().get("href").toString()
}#elementLayout
/// value when it's an element - an Element
local function of for Element(value: Unknown): Element? {
return value is JsValue and JsValue.hostIsElement(value) == true ? Self{node := value} : undefined
}/// the elements of a NodeList, an HTMLCollection
local function allOf for Element(list: Unknown): Array<Element> {
return JsValue.made(JsValue.hostList(list as JsValue)).filterMap(λ(item) Self.of(item))
}local function call for Element(self: Element, name: String, arguments: ..Unknown): Unknown {
return JsValue.made(self.node.call(name, ..arguments))
}global function from for Element(value: JsValue): Element? {
return Self.of(value)
}global method tag for Element(self: Element): String {
return self.node.get("tagName").toString().toLower()
}global method id for Element(self: Element): String {
return self.node.get("id").toString()
}global method text for Element(self: Element): String {
return (self.node.get("textContent") as String?) ?? ""
}global method setText for Element(self: Element, text: String): Undefined {
self.node.set("textContent", text)
}global method html for Element(self: Element): Html {
return Html{markup := self.node.get("innerHTML").toString()}
}global method setHtml for Element(self: Element, markup: Html): Undefined {
self.node.set("innerHTML", markup.markup)
}global method attribute for Element(self: Element, name: String): String? {
return self.call("getAttribute", name) as String?
}global method setAttribute for Element(self: Element, name: String, value: String): Undefined {
self.call("setAttribute", name, value)
}global method removeAttribute for Element(self: Element, name: String): Undefined {
self.call("removeAttribute", name)
}global method hasAttribute for Element(self: Element, name: String): Boolean {
return self.call("hasAttribute", name) == true
}global method value for Element(self: Element): String {
return (self.node.get("value") as String?) ?? ""
}global method setValue for Element(self: Element, value: String): Undefined {
self.node.set("value", value)
}global method isChecked for Element(self: Element): Boolean {
return self.node.get("checked") == true
}global method setChecked for Element(self: Element, checked: Boolean): Undefined {
self.node.set("checked", checked)
}global method setDisabled for Element(self: Element, disabled: Boolean): Undefined {
if disabled then { self.setAttribute("disabled", "") } else { self.removeAttribute("disabled") }
}local function classList for Element(self: Element): JsValue {
return self.node.get("classList") as JsValue
}global method classes for Element(self: Element): Array<String> {
return self.classList().items().map(λ(name) name.toString())
}global method hasClass for Element(self: Element, name: String): Boolean {
return JsValue.made(self.classList().call("contains", name)) == true
}global method addClass for Element(self: Element, name: String): Undefined {
JsValue.made(self.classList().call("add", name))
}global method removeClass for Element(self: Element, name: String): Undefined {
JsValue.made(self.classList().call("remove", name))
}global method toggleClass for Element(self: Element, name: String, on: Boolean? := undefined): Boolean {
let toggled := on == undefined ? self.classList().call("toggle", name) : self.classList().call("toggle", name, on)
return JsValue.made(toggled) == true
}global method style for Element(self: Element, property: String): String {
return JsValue.made((self.node.get("style") as JsValue).call("getPropertyValue", property)).toString()
}global method setStyle for Element(self: Element, property: String, value: String): Undefined {
JsValue.made((self.node.get("style") as JsValue).call("setProperty", property, value))
}global method computedStyle for Element(self: Element, property: String): String {
let computed := JsValue.made(self.node.get("ownerDocument") as JsValue).get("defaultView") as JsValue
return JsValue.made((JsValue.made(computed.call("getComputedStyle", self.node)) as JsValue).call("getPropertyValue", property)).toString()
}global method parent for Element(self: Element): Element? {
return Self.of(self.node.get("parentElement"))
}global method children for Element(self: Element): Array<Element> {
return Self.allOf(self.node.get("children"))
}global method next for Element(self: Element): Element? {
return Self.of(self.node.get("nextElementSibling"))
}global method previous for Element(self: Element): Element? {
return Self.of(self.node.get("previousElementSibling"))
}global method find for Element(self: Element, selector: String): Element? {
return Self.of(self.call("querySelector", selector))
}global method findAll for Element(self: Element, selector: String): Array<Element> {
return Self.allOf(self.call("querySelectorAll", selector))
}global method closest for Element(self: Element, selector: String): Element? {
return Self.of(self.call("closest", selector))
}global method matches for Element(self: Element, selector: String): Boolean {
return self.call("matches", selector) == true
}global method contains for Element(self: Element, other: Element): Boolean {
return self.call("contains", other) == true
}/// Html put in place by insertAdjacentHTML - parsed there; an Element or text by the DOM method itself,
/// so text stays text
local function insert for Element(self: Element, name: String, position: String, child: Element | String | Html): Undefined {
match child {
case Html as html then { self.call("insertAdjacentHTML", position, html.markup) }
case other then { self.call(name, other) }
}
}global method append for Element(self: Element, child: Element | String | Html): Undefined {
self.insert("append", "beforeend", child)
}global method prepend for Element(self: Element, child: Element | String | Html): Undefined {
self.insert("prepend", "afterbegin", child)
}global method before for Element(self: Element, sibling: Element | String | Html): Undefined {
self.insert("before", "beforebegin", sibling)
}global method after for Element(self: Element, sibling: Element | String | Html): Undefined {
self.insert("after", "afterend", sibling)
}global method replaceWith for Element(self: Element, other: Element | String | Html): Undefined {
match other {
case Html as html then {
self.call("insertAdjacentHTML", "afterend", html.markup)
self.remove()
}
case value then { self.call("replaceWith", value) }
}
}global method remove for Element(self: Element): Undefined {
self.call("remove")
}global method clear for Element(self: Element): Undefined {
self.call("replaceChildren")
}global method clone for Element(self: Element, deep: Boolean := true): Element {
return Self{node := self.call("cloneNode", deep) as JsValue}
}global method focus for Element(self: Element): Undefined {
self.call("focus")
}global method blur for Element(self: Element): Undefined {
self.call("blur")
}global method click for Element(self: Element): Undefined {
self.call("click")
}global method scrollIntoView for Element(self: Element): Undefined {
self.call("scrollIntoView")
}global method bounds for Element(self: Element): Rect {
let box := self.call("getBoundingClientRect") as JsValue
return Rect.new(box.get("x") as F64, box.get("y") as F64, box.get("width") as F64, box.get("height") as F64)
}global method on for Element(self: Element, kind: String, handler: Lambda<DomEvent, Undefined>): Listener {
return self.node.on(kind, handler)
}global method events for Element(self: Element, kind: String, limit: U64 := 1000): EventQueue {
return self.node.events(kind, limit)
}/// a CustomEvent when there's detail, an Event otherwise - both bubbling, and cancelable
global method dispatch for Element(self: Element, kind: String, detail: Unknown := undefined): Boolean {
let window := (self.node.get("ownerDocument") as JsValue).get("defaultView") as JsValue
let options := DispatchOptions{bubbles := true, cancelable := true, detail := JsValue.argument(detail)}
let event := JsValue.made((window.get(detail == undefined ? "Event" : "CustomEvent") as JsValue).construct(kind, options)) as JsValue
return self.call("dispatchEvent", event) == true
}global method canvas for Element(self: Element): IOError ! Canvas2d {
if self.tag() != "canvas" then {
error IOError.new("<" ++ self.tag() ++ "> isn't a <canvas>")
}
match self.node.call("getContext", "2d") {
case JsValue as context then { return Canvas2d{canvas := self.node, context} }
case _ then { error IOError.new("this page's <canvas> has no 2D drawing") }
}
}global method js for Element(self: Element): JsValue {
return self.node
}#equatable-for-elementthe same element - the host hands the same object back as the same JsValue
method equals(self: Element, other: Element): Boolean {
return self.node.handle == other.node.handle
}#tostring-for-element"
#domeventLayout
/// what the event is, read while it happens - a field its kind doesn't have left empty
local function of for DomEvent(event: JsValue): DomEvent {
let number := λ(name: String) (event.get(name) as F64?) ?? 0.0
let flag := λ(name: String) event.get(name) == true
let target := event.get("target")
let targetValue := target is JsValue ? (target as JsValue).get("value") : undefined
let targetChecked := target is JsValue ? (target as JsValue).get("checked") : undefined
return Self{
kind := event.get("type").toString(),
target := Element.of(target),
key := (event.get("key") as String?) ?? "",
code := (event.get("code") as String?) ?? "",
x := number("clientX"),
y := number("clientY"),
button := ((event.get("button") as F64?) ?? -1.0) as S64,
shift := flag("shiftKey"),
ctrl := flag("ctrlKey"),
alt := flag("altKey"),
meta := flag("metaKey"),
deltaX := number("deltaX"),
deltaY := number("deltaY"),
value := targetValue as String?,
checked := targetChecked as Boolean?,
repeat := flag("repeat"),
time := number("timeStamp"),
event
}
}global method preventDefault for DomEvent(self: DomEvent): Undefined {
self.event.call("preventDefault")
}global method stopPropagation for DomEvent(self: DomEvent): Undefined {
self.event.call("stopPropagation")
}global method js for DomEvent(self: DomEvent): JsValue {
return self.event
}#tostring-for-domevent"DomEvent click on
method toString(self: DomEvent): String {
return "DomEvent " ++ self.kind ++ (self.target == undefined ? "" : " on " ++ self.target.toString())
}#listenerLayout
uses #runtime.unlisten
global method remove for Listener(self: Listener): Undefined {
if self.active then {
set self.active := false
JsValue.hostUnlisten(self.number)
#runtime.unlisten(self.number)
}
}global method isActive for Listener(self: Listener): Boolean {
return self.active
}#tostring-for-listener"Listener click", "Listener click, removed"
method toString(self: Listener): String {
return "Listener " ++ self.kind ++ (self.active ? "" : ", removed")
}#eventqueueLayout
/// an event handed to the oldest next() waiting, or kept - the oldest kept dropped past the limit
local function add for EventQueue(self: EventQueue, event: DomEvent): Undefined {
let taker := self.takers.dequeue()
if taker != undefined then {
taker(event)
return
}
self.waiting.enqueue(event)
while self.waiting.size() > self.limit {
self.waiting.dequeue()
}
}global method next for EventQueue(self: EventQueue): Promise<DomEvent> {
let waiting := self.waiting.dequeue()
if waiting != undefined then {
return Promise.resolved(waiting)
}
let (promise, settle) := Promise<DomEvent>.withResolver()
self.takers.enqueue(settle)
return promise
}global method take for EventQueue(self: EventQueue): DomEvent? {
return self.waiting.dequeue()
}global method size for EventQueue(self: EventQueue): U64 {
return self.waiting.size()
} uses #runtime.unlisten
global method close for EventQueue(self: EventQueue): Undefined {
if not self.closed then {
set self.closed := true
JsValue.hostUnlisten(self.number)
#runtime.unlisten(self.number)
}
}global method isClosed for EventQueue(self: EventQueue): Boolean {
return self.closed
}#tostring-for-eventqueue"EventQueue click, 2 waiting"
method toString(self: EventQueue): String {
return "EventQueue " ++ self.kind ++ ", " ++ self.waiting.size().toString() ++ " waiting" ++ (self.closed ? ", closed" : "")
}#webstorageLayout
global method get for WebStorage(self: WebStorage, key: String): String? {
return JsValue.made(self.storage.call("getItem", key)) as String?
}global method set for WebStorage(self: WebStorage, key: String, value: String): Undefined {
JsValue.made(self.storage.call("setItem", key, value))
}global method remove for WebStorage(self: WebStorage, key: String): Undefined {
JsValue.made(self.storage.call("removeItem", key))
}global method clear for WebStorage(self: WebStorage): Undefined {
JsValue.made(self.storage.call("clear"))
}global method keys for WebStorage(self: WebStorage): Array<String> {
return (0..<self.size()).map(λ(i) JsValue.made(self.storage.call("key", i)).toString()).collect()
}global method size for WebStorage(self: WebStorage): U64 {
return self.storage.get("length") as U64
}#tostring-for-webstorage"WebStorage with 3 keys"
method toString(self: WebStorage): String {
let count := self.size()
return "WebStorage with " ++ count.toString() ++ (count == 1 ? " key" : " keys")
}#canvas2dLayout
local function draw for Canvas2d(self: Canvas2d, name: String, arguments: ..Unknown): Undefined {
JsValue.made(self.context.call(name, ..arguments))
}/// a Color as CSS writes it - rgb(255 0 0 / 0.5) - or CSS text as it is
local function css for Canvas2d(color: Color | String): String {
return match color {
case Color as c then "rgb(" ++ c.red().toString() ++ " " ++ c.green().toString() ++ " " ++ c.blue().toString() ++ " / " ++ ((c.alpha() as F64) / 255.0).toString() ++ ")"
case String as text then text
}
}global method width for Canvas2d(self: Canvas2d): U64 {
return self.canvas.get("width") as U64
}global method height for Canvas2d(self: Canvas2d): U64 {
return self.canvas.get("height") as U64
}global method resize for Canvas2d(self: Canvas2d, width: U64, height: U64): Undefined {
self.canvas.set("width", width)
self.canvas.set("height", height)
}global method clear for Canvas2d(self: Canvas2d): Undefined {
self.draw("clearRect", 0.0, 0.0, self.width() as F64, self.height() as F64)
}global method setFill for Canvas2d(self: Canvas2d, color: Color | String): Undefined {
self.context.set("fillStyle", Self.css(color))
}global method setStroke for Canvas2d(self: Canvas2d, color: Color | String): Undefined {
self.context.set("strokeStyle", Self.css(color))
}global method setLineWidth for Canvas2d(self: Canvas2d, width: F64): Undefined {
self.context.set("lineWidth", width)
}global method setFont for Canvas2d(self: Canvas2d, font: String): Undefined {
self.context.set("font", font)
}global method setAlpha for Canvas2d(self: Canvas2d, alpha: F64): Undefined {
self.context.set("globalAlpha", alpha)
}global method fillRect for Canvas2d(self: Canvas2d, x: F64, y: F64, width: F64, height: F64): Undefined {
self.draw("fillRect", x, y, width, height)
}global method strokeRect for Canvas2d(self: Canvas2d, x: F64, y: F64, width: F64, height: F64): Undefined {
self.draw("strokeRect", x, y, width, height)
}global method clearRect for Canvas2d(self: Canvas2d, x: F64, y: F64, width: F64, height: F64): Undefined {
self.draw("clearRect", x, y, width, height)
}global method fillText for Canvas2d(self: Canvas2d, text: String, x: F64, y: F64): Undefined {
self.draw("fillText", text, x, y)
}global method measureText for Canvas2d(self: Canvas2d, text: String): F64 {
return (JsValue.made(self.context.call("measureText", text)) as JsValue).get("width") as F64
}global method beginPath for Canvas2d(self: Canvas2d): Undefined {
self.draw("beginPath")
}global method moveTo for Canvas2d(self: Canvas2d, x: F64, y: F64): Undefined {
self.draw("moveTo", x, y)
}global method lineTo for Canvas2d(self: Canvas2d, x: F64, y: F64): Undefined {
self.draw("lineTo", x, y)
}global method arc for Canvas2d(self: Canvas2d, x: F64, y: F64, radius: F64, start: F64 := 0.0, end: F64 := 6.283185307179586): Undefined {
self.draw("arc", x, y, radius, start, end)
}global method closePath for Canvas2d(self: Canvas2d): Undefined {
self.draw("closePath")
}global method fill for Canvas2d(self: Canvas2d): Undefined {
self.draw("fill")
}global method stroke for Canvas2d(self: Canvas2d): Undefined {
self.draw("stroke")
}global method save for Canvas2d(self: Canvas2d): Undefined {
self.draw("save")
}global method restore for Canvas2d(self: Canvas2d): Undefined {
self.draw("restore")
}global method translate for Canvas2d(self: Canvas2d, x: F64, y: F64): Undefined {
self.draw("translate", x, y)
}global method rotate for Canvas2d(self: Canvas2d, angle: F64): Undefined {
self.draw("rotate", angle)
}global method scale for Canvas2d(self: Canvas2d, x: F64, y: F64): Undefined {
self.draw("scale", x, y)
}/// the pixels as RGBA bytes, row by row, in an ImageData put on the canvas as they are
global method drawImage for Canvas2d(self: Canvas2d, image: Image, x: F64, y: F64): Undefined {
let rgba := Bytes.filled(image.width() * image.height() * 4)
for py in 0..<image.height() {
for px in 0..<image.width() {
let color := image.pixel(px, py)!!
let at := (py * image.width() + px) * 4
set rgba[at] := color.red()
set rgba[at + 1] := color.green()
set rgba[at + 2] := color.blue()
set rgba[at + 3] := color.alpha()
}
}
let window := (self.canvas.get("ownerDocument") as JsValue).get("defaultView") as JsValue
let data := JsValue.made((window.get("ImageData") as JsValue).construct(rgba, image.width(), image.height()))
self.draw("putImageData", data, x, y)
}global method pixels for Canvas2d(self: Canvas2d): Image {
let data := JsValue.made(self.context.call("getImageData", 0, 0, self.width(), self.height())) as JsValue
let rgba := (data.get("data") as JsValue).items()
return Image.generate(self.width(), self.height(), λ(x, y) {
let at := (y * self.width() + x) * 4
return Color.rgba(rgba[at] as U8, rgba[at + 1] as U8, rgba[at + 2] as U8, rgba[at + 3] as U8)
})
}global method js for Canvas2d(self: Canvas2d): JsValue {
return self.context
}#tostring-for-canvas2d"Canvas2d 640x480"
method toString(self: Canvas2d): String {
return "Canvas2d " ++ self.width().toString() ++ "x" ++ self.height().toString()
}#range-typeLayout
/// the last value the range reaches - undefined when it reaches none
local method finalValue for Range<Type>(self: Range<Type>): Type? {
if self.inclusive then { return self.first <= self.last ? self.last : undefined }
return self.first < self.last ? self.last.previous() : undefined
}global function new for Range<Type>(start: Type, end: Type, inclusive: Boolean := false): Range<Type> {
return Range<Type>{first := start, last := end, inclusive := inclusive}
}global method start for Range<Type>(self: Range<Type>): Type {
return self.first
}global method end for Range<Type>(self: Range<Type>): Type {
return self.last
}global method isInclusive for Range<Type>(self: Range<Type>): Boolean {
return self.inclusive
}/// every by-th value from the start - stops on the final value itself, so stepping never runs past
/// Type's own largest value
global method step for Range<Type>(self: Range<Type>, by: U64): Iterator<Type> {
let final := self.finalValue()
if final == undefined or by == 0 then { return }
let current := self.first
for _ in forever() {
yield current
for _ in 0..<by {
if current == final then { return }
set current := current.next()
}
}
}/// from the final value back to the start
global method reverse for Range<Type>(self: Range<Type>): Iterator<Type> {
let final := self.finalValue()
if final == undefined then { return }
let current := final
for _ in forever() {
yield current
if current == self.first then { return }
set current := current.previous()
}
}global method overlaps for Range<Type>(self: Range<Type>, other: Range<Type>): Boolean {
return self.intersection(other) != undefined
}global method intersection for Range<Type>(self: Range<Type>, other: Range<Type>): Range<Type>? {
let (a, b) := (self.finalValue(), other.finalValue())
if a == undefined or b == undefined then { return undefined }
let start := self.first.max(other.first)
let end := a.min(b)
return start <= end ? Range.new(start, end, true) : undefined
}/// the nearest value inside - value itself when it's already in
global method clamp for Range<Type>(self: Range<Type>, value: Type): Type {
if value < self.first then { return self.first }
let final := self.finalValue() ?? self.first
return value > final ? final : value
}global method toArray for Range<Type>(self: Range<Type>): Array<Type> {
return self.iterator().collect()
}global method toInclusive for Range<Type>(self: Range<Type>): Range<Type> {
return self.inclusive ? self : Range.new(self.first, self.finalValue() ?? self.first, true)
}#equatable-for-range-typemethod equals(self: Range<Type>, other: Range<Type>): Boolean {
return self.first == other.first and self.last == other.last and self.inclusive == other.inclusive
}#hashable-for-range-typemethod hash(self: Range<Type>): U64 {
return hashCombine(hashCombine(self.first.hash(), self.last.hash()), self.inclusive.hash())
}#tostring-for-range-type/// "1..<5" or "1..=5"
method toString(self: Range<Type>): String {
return self.first.toString() ++ (self.inclusive ? "..=" : "..<") ++ self.last.toString()
}#iterable-type-for-range-typemethod iterator(self: Range<Type>): Iterator<Type> {
return self.step(1)
}#collection-type-for-range-typemethod contains(self: Range<Type>, value: Type): Boolean {
return value >= self.first and (self.inclusive ? value <= self.last : value < self.last)
}#sized-type-for-range-type/// counted by stepping - for a number type, the compiler folds this to end - start
method size(self: Range<Type>): U64 {
return self.count(λ(_) true)
}method isEmpty(self: Range<Type>): Boolean {
return self.finalValue() == undefined
}#copy-for-range-typeNothing to write - a marker interface, or one another block already covers.
#iterator-typepulls one item at a time from a Lambda - for a generator, the compiler turns its body into that Lambda, resuming after the last yield each time it's called
Layout
global function empty for Iterator<Type>(): Iterator<Type> {
return Iterator.fromLambda(λ undefined)
}global function once for Iterator<Type>(item: Type): Iterator<Type> {
yield item
}global function repeat for Iterator<Type>(item: Type): Iterator<Type> {
for _ in forever() {
yield item
}
}global function iterate for Iterator<Type>(seed: Type, next: Lambda<Type, Type>): Iterator<Type> {
let current := seed
for _ in forever() {
yield current
set current := next(current)
}
}/// what `start..<∞` and `start..=∞` are
global function countFrom for Iterator<Type: Numeric>(start: Type): Iterator<Type> {
let current := start
for _ in forever() {
yield current
set current := current + (1 as Type)
}
}global function fromLambda for Iterator<Type>(next: Lambda<(), Type?>): Iterator<Type> {
return Iterator<Type>{pull := next, buffered := undefined, hasBuffered := false}
}global method next for Iterator<Type>(self: Iterator<Type>): Type? {
if not self.hasBuffered then { return self.pull() }
set self.hasBuffered := false
return self.buffered
}global method peek for Iterator<Type>(self: Iterator<Type>): Type? {
if not self.hasBuffered then {
set self.buffered := self.pull()
set self.hasBuffered := true
}
return self.buffered
}global method map for Iterator<Type, Result>(self: Iterator<Type>, f: Lambda<Type, Result>): Iterator<Result> {
for item in self {
yield f(item)
}
}global method flatMap for Iterator<Type, Result>(self: Iterator<Type>, f: Lambda<Type, Iterable<Result>>): Iterator<Result> {
for item in self {
for inner in f(item) {
yield inner
}
}
}global method filter for Iterator<Type>(self: Iterator<Type>, keep: Lambda<Type, Boolean>): Iterator<Type> {
for item in self if keep(item) {
yield item
}
}global method filterMap for Iterator<Type, Result>(self: Iterator<Type>, f: Lambda<Type, Result?>): Iterator<Result> {
for item in self {
let mapped := f(item)
if mapped != undefined then { yield mapped }
}
}/// stops right after the count-th item, so it never pulls one more than it gives
global method take for Iterator<Type>(self: Iterator<Type>, count: U64): Iterator<Type> {
if count == 0 then { return }
for item, i in self {
yield item
if i + 1 == count then { return }
}
}global method drop for Iterator<Type>(self: Iterator<Type>, count: U64): Iterator<Type> {
for item, i in self if i >= count {
yield item
}
}global method takeWhile for Iterator<Type>(self: Iterator<Type>, where: Lambda<Type, Boolean>): Iterator<Type> {
for item in self until not where(item) {
yield item
}
}/// once one item is let through, where isn't asked again
global method dropWhile for Iterator<Type>(self: Iterator<Type>, where: Lambda<Type, Boolean>): Iterator<Type> {
let dropping := true
for item in self if not dropping or not where(item) {
set dropping := false
yield item
}
}global method stepBy for Iterator<Type>(self: Iterator<Type>, step: U64): Iterator<Type> {
for item, i in self if i % step.max(1) == 0 {
yield item
}
}global method chain for Iterator<Type>(self: Iterator<Type>, other: Iterable<Type>): Iterator<Type> {
for item in self {
yield item
}
for item in other {
yield item
}
}/// as long as the shorter side
global method zip for Iterator<Type, Other>(self: Iterator<Type>, other: Iterable<Other>): Iterator<(Type, Other)> {
let others := other.iterator()
for item in self {
let paired := others.next()
if paired == undefined then { return }
yield (item, paired)
}
}global method enumerate for Iterator<Type>(self: Iterator<Type>): Iterator<(U64, Type)> {
for item, i in self {
yield (i, item)
}
}/// the last chunk is shorter when the items run out part way
global method chunks for Iterator<Type>(self: Iterator<Type>, size: U64): Iterator<Array<Type>> {
let chunk: Array<Type> := []
for item in self {
chunk.push(item)
if chunk.size() == size.max(1) then {
yield chunk
set chunk := []
}
}
if not chunk.isEmpty() then { yield chunk }
}/// keeps the last size items, and hands out each full set as it forms
global method windows for Iterator<Type>(self: Iterator<Type>, size: U64): Iterator<Window<Type>> {
let recent: Array<Type> := []
for item, i in self {
recent.push(item)
if recent.size() > size then { recent.popFront() }
if size > 0 and recent.size() == size then { yield Window<Type>{source := recent.clone(), offset := 0, width := size, position := i + 1 - size} }
}
}global method scan for Iterator<Type, State>(self: Iterator<Type>, initial: State, f: Lambda<(State, Type), State>): Iterator<State> {
let state := initial
for item in self {
set state := f(state, item)
yield state
}
}global method inspect for Iterator<Type>(self: Iterator<Type>, f: Lambda<Type, Undefined>): Iterator<Type> {
for item in self {
f(item)
yield item
}
}/// the first pass is remembered, then replayed forever
global method cycle for Iterator<Type>(self: Iterator<Type>): Iterator<Type> {
let seen: Array<Type> := []
for item in self {
seen.push(item)
yield item
}
if seen.isEmpty() then { return }
for _ in forever() {
for item in seen {
yield item
}
}
}/// Type: Hashable
global method distinct for Iterator<Type>(self: Iterator<Type>): Iterator<Type> {
let seen: Set<Type> := Set.of()
for item in self if seen.add(item) {
yield item
}
}global method nth for Iterator<Type>(self: Iterator<Type>, index: U64): Type? {
return self.drop(index).next()
}global method last for Iterator<Type>(self: Iterator<Type>): Type? {
let result: Type? := undefined
for item in self {
set result := item
}
return result
}global method collect for Iterator<Type>(self: Iterator<Type>): Array<Type> {
return self.toArray()
}global method collectInto for Iterator<Type, Target: FromIterable<Type>>(self: Iterator<Type>): Target {
return Target.fromIterable(self)
}global method distinctBy for Iterator<Type, Key: Hashable>(self: Iterator<Type>, key: Lambda<Type, Key>): Iterator<Type> {
let seen: Set<Key> := Set.of()
for item in self if seen.add(key(item)) {
yield item
}
}global method chunkBy for Iterator<Type, Key>(self: Iterator<Type>, key: Lambda<Type, Key>): Iterator<Array<Type>> {
let run: Array<Type> := []
let runKey: Key? := undefined
for item in self {
let k := key(item)
if not run.isEmpty() and k != runKey then {
yield run
set run := []
}
run.push(item)
set runKey := k
}
if not run.isEmpty() then {
yield run
}
}#iterable-type-for-iterator-type/// itself - iterating an Iterator uses it up
method iterator(self: Iterator<Type>): Iterator<Type> {
return self
}#cursor-typea position over an indexed sequence - the source is shared, not copied
Layout
global function over for Cursor<Type>(source: Indexable<U64, Type> & Sized<Type>): Cursor<Type> {
return Cursor<Type>{source := source, at := 0}
}global method position for Cursor<Type>(self: Cursor<Type>): U64 {
return self.at
}global method current for Cursor<Type>(self: Cursor<Type>): Type? {
return self.source[self.at]
}global method peek for Cursor<Type>(self: Cursor<Type>, offset: U64 := 1): Type? {
return self.source[self.at + offset]
}global method previous for Cursor<Type>(self: Cursor<Type>): Type? {
return self.at == 0 ? undefined : self.source[self.at - 1]
}global method advance for Cursor<Type>(self: Cursor<Type>, count: U64 := 1): Undefined {
set self.at := (self.at + count).min(self.source.size())
}global method retreat for Cursor<Type>(self: Cursor<Type>, count: U64 := 1): Undefined {
set self.at -= count.min(self.at)
}global method seek for Cursor<Type>(self: Cursor<Type>, position: U64): IndexError ! Undefined {
if position > self.source.size() then { error IndexError.new("position " ++ position.toString() ++ " is past the end of " ++ self.source.size().toString() ++ " items") }
set self.at := position
}global method isAtStart for Cursor<Type>(self: Cursor<Type>): Boolean {
return self.at == 0
}global method isAtEnd for Cursor<Type>(self: Cursor<Type>): Boolean {
return self.at >= self.source.size()
}global method remaining for Cursor<Type>(self: Cursor<Type>): U64 {
return self.source.size() - self.at.min(self.source.size())
}global method consume for Cursor<Type>(self: Cursor<Type>, item: Type): Boolean {
if self.isAtEnd() or self.current() != item then { return false }
self.advance()
return true
}global method consumeWhile for Cursor<Type>(self: Cursor<Type>, where: Lambda<Type, Boolean>): Array<Type> {
let taken: Array<Type> := []
for _ in forever() while not self.isAtEnd() and where(self.current() as Type) {
taken.push(self.current() as Type)
self.advance()
}
return taken
}global method mark for Cursor<Type>(self: Cursor<Type>): U64 {
return self.at
}global method rest for Cursor<Type>(self: Cursor<Type>): Array<Type> {
return Array.generate(self.remaining(), λ(i) self.source[self.at + i] as Type)
}global method sliceFrom for Cursor<Type>(self: Cursor<Type>, start: U64): Array<Type> {
return Array.generate(self.at - start.min(self.at), λ(i) self.source[start + i] as Type)
}#equatable-for-cursor-typemethod equals(self: Cursor<Type>, other: Cursor<Type>): Boolean {
return self.source == other.source and self.at == other.at
}#clone-for-cursor-typea new position over the same source
method clone(self: Cursor<Type>): Cursor<Type> {
return Cursor<Type>{source := self.source, at := self.at}
}#window-typewidth items of source from offset on - a view, not a copy; position is where it starts in the sequence it was made from (for an Iterator's windows, source is a copy and offset is 0)
Layout
global method start for Window<Type>(self: Window<Type>): U64 {
return self.position
}global method end for Window<Type>(self: Window<Type>): U64 {
return self.position + self.width
}global method first for Window<Type>(self: Window<Type>): Type {
return self.source[self.offset] as Type
}global method last for Window<Type>(self: Window<Type>): Type {
return self.source[self.offset + self.width - 1] as Type
}global method toArray for Window<Type>(self: Window<Type>): Array<Type> {
return Array.generate(self.width, λ(i) self.source[self.offset + i] as Type)
}#iterable-type-for-window-typemethod iterator(self: Window<Type>): Iterator<Type> {
for i in 0..<self.width {
yield self.source[self.offset + i] as Type
}
}#sized-type-for-window-typemethod size(self: Window<Type>): U64 {
return self.width
}method isEmpty(self: Window<Type>): Boolean {
return self.width == 0
}#indexable-u64-type-for-window-typemethod get(self: Window<Type>, key: U64): Type? {
return key < self.width ? self.source[self.offset + key] : undefined
}/// writes through to the source
method set(self: Window<Type>, key: U64, value: Type): Error ! Undefined {
if key >= self.width then { error IndexError.new("index " ++ key.toString() ++ " is past a window of " ++ self.width.toString()) }
set self.source[self.offset + key] := value
}#equatable-for-window-typemethod equals(self: Window<Type>, other: Window<Type>): Boolean {
return self.toArray() == other.toArray()
}#durationa whole number of nanoseconds - S128 holds about 5 * 10^21 years either way
Layout
local const perMicrosecond: S128 := 1_000local const perMillisecond: S128 := 1_000_000local const perSecond: S128 := 1_000_000_000local const perMinute: S128 := 60_000_000_000local const perHour: S128 := 3_600_000_000_000local const perDay: S128 := 86_400_000_000_000local function fromFloat for Duration(count: F64, unit: S128): Duration {
return Duration{nanos := (count * (unit as F64)).round() as S128}
}global function nanoseconds for Duration(count: S64): Duration {
return Duration{nanos := count as S128}
}global function microseconds for Duration(count: S64): Duration {
return Duration{nanos := (count as S128) * perMicrosecond}
}global function milliseconds for Duration(count: S64): Duration {
return Duration{nanos := (count as S128) * perMillisecond}
}global function seconds for Duration(count: F64): Duration {
return Self.fromFloat(count, perSecond)
}global function minutes for Duration(count: F64): Duration {
return Self.fromFloat(count, perMinute)
}global function hours for Duration(count: F64): Duration {
return Self.fromFloat(count, perHour)
}global function days for Duration(count: F64): Duration {
return Self.fromFloat(count, perDay)
}global method toNanoseconds for Duration(self: Duration): S128 {
return self.nanos
}global method toMilliseconds for Duration(self: Duration): F64 {
return (self.nanos as F64) / (perMillisecond as F64)
}global method toSeconds for Duration(self: Duration): F64 {
return (self.nanos as F64) / (perSecond as F64)
}global method toMinutes for Duration(self: Duration): F64 {
return (self.nanos as F64) / (perMinute as F64)
}global method toHours for Duration(self: Duration): F64 {
return (self.nanos as F64) / (perHour as F64)
}global method toDays for Duration(self: Duration): F64 {
return (self.nanos as F64) / (perDay as F64)
}/// of the absolute length - days carries the sign
global method parts for Duration(self: Duration): (days: S64, hours: U8, minutes: U8, seconds: U8, nanoseconds: U32) {
let rest := self.nanos.abs()
let days := rest / perDay
let sign: S128 := self.nanos < 0 ? -1 : 1
return (
days := (days * sign) as S64,
hours := ((rest % perDay) / perHour) as U8,
minutes := ((rest % perHour) / perMinute) as U8,
seconds := ((rest % perMinute) / perSecond) as U8,
nanoseconds := (rest % perSecond) as U32,
)
}global method abs for Duration(self: Duration): Duration {
return Duration{nanos := self.nanos.abs()}
}global method isNegative for Duration(self: Duration): Boolean {
return self.nanos < 0
}global method isZero for Duration(self: Duration): Boolean {
return self.nanos == 0
}/// to the nearest multiple of unit, halves away from zero
global method roundTo for Duration(self: Duration, unit: Duration): Duration {
if unit.nanos == 0 then { return self }
let step := unit.nanos.abs()
let (whole, rest) := (self.nanos / step, self.nanos % step)
let nudge: S128 := rest.abs() * 2 >= step ? (self.nanos < 0 ? -1 : 1) : 0
return Duration{nanos := (whole + nudge) * step}
}/// "PT1H30M", "P2DT3H", "-PT0.5S" - zero is "PT0S"
global method toIsoString for Duration(self: Duration): String {
if self.nanos == 0 then { return "PT0S" }
let p := self.parts()
let days := p.days.abs()
let seconds := p.seconds == 0 and p.nanoseconds == 0 ? "" : (p.seconds as F64 + (p.nanoseconds as F64) / 1e9).toString() ++ "S"
let time := (p.hours > 0 ? p.hours.toString() ++ "H" : "") ++ (p.minutes > 0 ? p.minutes.toString() ++ "M" : "") ++ seconds
return (self.nanos < 0 ? "-P" : "P") ++ (days > 0 ? days.toString() ++ "D" : "") ++ (time.isEmpty() ? "" : "T" ++ time)
}#equatable-for-durationmethod equals(self: Duration, other: Duration): Boolean {
return self.nanos == other.nanos
}#hashable-for-durationmethod hash(self: Duration): U64 {
return self.nanos.hash()
}#comparable-for-durationmethod compare(self: Duration, other: Duration): Ordering {
return self.nanos.compare(other.nanos)
}#tostring-for-duration/// the non-zero parts, largest first - "1h 2m 3.5s", "250ms", "0s"
method toString(self: Duration): String {
if self.nanos == 0 then { return "0s" }
let p := self.parts()
let pieces: Array<String> := []
if p.days != 0 then { pieces.push(p.days.abs().toString() ++ "d") }
if p.hours != 0 then { pieces.push(p.hours.toString() ++ "h") }
if p.minutes != 0 then { pieces.push(p.minutes.toString() ++ "m") }
if p.seconds != 0 then {
pieces.push((p.seconds as F64 + (p.nanoseconds as F64) / 1e9).toString() ++ "s")
} else {
if p.nanoseconds != 0 then { pieces.push(((p.nanoseconds as F64) / 1e6).toString() ++ "ms") }
}
return (self.nanos < 0 ? "-" : "") ++ pieces.join(" ")
}#fromstring-for-duration/// a run of number-unit pairs ("1h30m", "250ms", "-1.5s"), or ISO-8601 ("PT1H30M", "P2DT3H")
function parse(text: String): ParseError ! Duration {
let trimmed := text.trim()
let negative := trimmed.startsWith("-")
let body := trimmed.stripPrefix("-") ?? trimmed
let iso := body.startsWith("P")
let pattern := iso ? Regex("([0-9.]+)([DHMS])|(T)")!! : Regex("([0-9.]+)(ns|us|µs|ms|s|m|h|d)")!!
let total: F64 := 0
let afterT := false
let consumed: U64 := iso ? 1 : 0
for found in pattern.findAll(body) {
if found.start() != consumed then { error ParseError.new("not a duration: " ++ text) }
set consumed := found.end()
if found.group(3) != undefined then {
set afterT := true
} else {
let amount := F64.parse(found.group(1) ?? "")!!
let unit := found.group(2) ?? ""
/// "M" before the T is months, which aren't a fixed length - it's left out of the table
let perUnit := Map.of(
("d", perDay), ("D", perDay), ("h", perHour), ("H", perHour), ("m", perMinute), ("s", perSecond), ("S", perSecond),
("ms", perMillisecond), ("us", perMicrosecond), ("µs", perMicrosecond), ("ns", 1 as S128),
).get(unit) ?? (unit == "M" and afterT ? perMinute : 0)
if perUnit == 0 then { error ParseError.new("months and years aren't fixed lengths of time: " ++ text) }
set total += amount * (perUnit as F64)
}
}
if consumed != body.size() or consumed == 0 then { error ParseError.new("not a duration: " ++ text) }
return Duration{nanos := (negative ? -total : total).round() as S128}
}#numeric-for-durationarithmetic on the nanosecond counts - Duration * Duration is rarely what's meant; scale by a number instead (d * 1.5 is Scalable)
function zero(): Duration {
return Duration{nanos := 0}
}function one(): Duration {
return Duration{nanos := 1}
}method add(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos + other.nanos}
}method subtract(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos - other.nanos}
}method multiply(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos * other.nanos}
}method divide(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos / other.nanos}
}method floorDivide(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos // other.nanos}
}method remainder(self: Duration, other: Duration): Duration {
return Duration{nanos := self.nanos % other.nanos}
}method power(self: Duration, exponent: Duration): Duration {
return Duration{nanos := self.nanos ^ exponent.nanos}
}method negate(self: Duration): Duration {
return Duration{nanos := -self.nanos}
}method plus(self: Duration): Duration {
return self
}#scalable-f64-for-durationmethod scale(self: Duration, factor: F64): Duration {
return Duration{nanos := ((self.nanos as F64) * factor).round() as S128}
}#default-for-durationfunction default(): Duration {
return Duration.zero()
}#copy-for-durationNothing to write - a marker interface, or one another block already covers.
#instanthow long after 1970-01-01T00:00:00Z, in UTC
Layout
/// days since 1970-01-01 to a (year, month, day) in the proleptic Gregorian calendar - and back.
/// Howard Hinnant's algorithms: shift the year to start in March, so the leap day comes last
local function civilFromDays for Instant(days: S64): (S64, U8, U8) {
let z := days + 719468
let era := z // 146097
let dayOfEra := z - era * 146097
let yearOfEra := (dayOfEra - dayOfEra / 1460 + dayOfEra / 36524 - dayOfEra / 146096) / 365
let dayOfYear := dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100)
let shiftedMonth := (5 * dayOfYear + 2) / 153
let day := dayOfYear - (153 * shiftedMonth + 2) / 5 + 1
let month := shiftedMonth < 10 ? shiftedMonth + 3 : shiftedMonth - 9
return (yearOfEra + era * 400 + (month <= 2 ? 1 : 0), month as U8, day as U8)
}local function daysFromCivil for Instant(year: S64, month: U8, day: U8): S64 {
let y := month <= 2 ? year - 1 : year
let era := y // 400
let yearOfEra := y - era * 400
let m := month as S64
let dayOfYear := (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + (day as S64) - 1
let dayOfEra := yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear
return era * 146097 + dayOfEra - 719468
}/// (year, month, day, hour, minute, second, nanosecond) at a fixed offset from UTC
local method fields for Instant(self: Instant, offset: Duration): (S64, U8, U8, U8, U8, U8, U32) {
let local := (self.sinceEpoch + offset).toNanoseconds()
let perDay: S128 := 86_400_000_000_000
let days := (local // perDay) as S64
let inDay := local.modulo(perDay)
let (year, month, day) := Self.civilFromDays(days)
return (year, month, day, (inDay / 3_600_000_000_000) as U8, ((inDay / 60_000_000_000) % 60) as U8, ((inDay / 1_000_000_000) % 60) as U8, (inDay % 1_000_000_000) as U32)
}global function epoch for Instant(): Instant {
return Instant{sinceEpoch := Duration.zero()}
}global function fromUnixSeconds for Instant(seconds: F64): Instant {
return Instant{sinceEpoch := Duration.seconds(seconds)}
}global function fromUnixMilliseconds for Instant(milliseconds: S64): Instant {
return Instant{sinceEpoch := Duration.milliseconds(milliseconds)}
}global method add for Instant(self: Instant, duration: Duration): Instant {
return Instant{sinceEpoch := self.sinceEpoch + duration}
}global method subtract for Instant(self: Instant, duration: Duration): Instant {
return Instant{sinceEpoch := self.sinceEpoch - duration}
}global method since for Instant(self: Instant, earlier: Instant): Duration {
return self.sinceEpoch - earlier.sinceEpoch
}global method until for Instant(self: Instant, later: Instant): Duration {
return later.sinceEpoch - self.sinceEpoch
}global method isBefore for Instant(self: Instant, other: Instant): Boolean {
return self.sinceEpoch < other.sinceEpoch
}global method isAfter for Instant(self: Instant, other: Instant): Boolean {
return self.sinceEpoch > other.sinceEpoch
}global method toUnixSeconds for Instant(self: Instant): F64 {
return self.sinceEpoch.toSeconds()
}global method toUnixMilliseconds for Instant(self: Instant): S64 {
return (self.sinceEpoch.toNanoseconds() // 1_000_000) as S64
}/// down to a multiple of unit since the epoch - before the epoch too, toward the past
global method truncate for Instant(self: Instant, unit: Duration): Instant {
let step := unit.toNanoseconds().abs()
if step == 0 then { return self }
let nanos := self.sinceEpoch.toNanoseconds()
return Instant{sinceEpoch := Duration.nanoseconds((nanos - nanos.modulo(step)) as S64)}
}/// YYYY, MM, DD, hh, mm, ss and SSS (milliseconds) are replaced - everything else is kept as written
global method format for Instant(self: Instant, pattern: String, offset: Duration := Duration.zero()): String {
let (year, month, day, hour, minute, second, nano) := self.fields(offset)
let two := λ(n) n.toString().padStart(2, '0')
let replacements := [
("YYYY", year.toString().padStart(4, '0')),
("MM", two(month)),
("DD", two(day)),
("hh", two(hour)),
("mm", two(minute)),
("ss", two(second)),
("SSS", (nano / 1_000_000).toString().padStart(3, '0')),
]
return replacements.reduce(pattern, λ(text, pair) text.replaceAll(pair[0], pair[1]))
}/// the wall clock in zone at self
global method toDateTime for Instant(self: Instant, zone: TimeZone := &timeZone): DateTime {
let local := (self.sinceEpoch + zone.offsetAt(self)).toNanoseconds()
let perDay: S128 := 86_400_000_000_000
return DateTime{date := Date.fromDays((local // perDay) as S64), time := TimeOfDay{sinceMidnight := Duration.nanoseconds(local.modulo(perDay) as S64)}}
}global method toDate for Instant(self: Instant, zone: TimeZone := &timeZone): Date {
return self.toDateTime(zone).date()
}#tostring-for-instant"2024-05-01T12:30:00Z", with as many fraction digits as it needs (none, for a whole second)
method toString(self: Instant): String {
let (year, month, day, hour, minute, second, nano) := self.fields(Duration.zero())
let two := λ(n) n.toString().padStart(2, '0')
let digits := nano.toString().padStart(9, '0').codepoints().reverse().dropWhile(λ(c) c == '0').reverse()
let fraction := nano == 0 ? "" : "." ++ String.fromCodepoints(digits)
let date := [year.toString().padStart(4, '0'), two(month), two(day)].join("-")
let time := [two(hour), two(minute), two(second)].join(":")
return date ++ "T" ++ time ++ fraction ++ "Z"
}#equatable-for-instantmethod equals(self: Instant, other: Instant): Boolean {
return self.sinceEpoch == other.sinceEpoch
}#hashable-for-instantmethod hash(self: Instant): U64 {
return self.sinceEpoch.hash()
}#comparable-for-instantmethod compare(self: Instant, other: Instant): Ordering {
return self.sinceEpoch.compare(other.sinceEpoch)
}#fromstring-for-instant/// "2024-05-01", "2024-05-01T12:30:00Z", "2024-05-01 12:30:00.25+02:00" - no offset means UTC
function parse(text: String): ParseError ! Instant {
let pattern := Regex("^(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})(?:[T ]([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\\.([0-9]{1,9}))?)?)?(Z|[+-][0-9]{2}:?[0-9]{2})?quot;)!!
let found := (pattern.find(text.trim()) ?! ParseError.new("not an ISO-8601 date and time: " ++ text))!!
let number := λ(group) S64.parse(found.group(group) ?? "0") !? 0
let month := number(2) as U8
let day := number(3) as U8
if month < 1 or month > 12 or day < 1 or day > 31 then { error ParseError.new("no such date: " ++ text) }
let fraction := (found.group(7) ?? "").padEnd(9, '0')
let zone := found.group(8) ?? "Z"
let zoneDigits := zone.replaceAll(":", "")
let offsetMinutes := zone == "Z" ? 0 : (zoneDigits.startsWith("-") ? -1 : 1) * (S64.parse(zoneDigits.slice(1..<3) !? "0") !? 0) * 60 + (S64.parse(zoneDigits.slice(3..<5) !? "0") !? 0)
let days := Self.daysFromCivil(number(1), month, day)
let seconds := days * 86400 + number(4) * 3600 + number(5) * 60 + number(6) - offsetMinutes * 60
return Instant{sinceEpoch := Duration.nanoseconds(seconds * 1_000_000_000 + (S64.parse(fraction) !? 0))}
}#copy-for-instantNothing to write - a marker interface, or one another block already covers.
#datedays since 1970-01-01 - the calendar of Instant.civilFromDays and Instant.daysFromCivil
Layout
global function of for Date(year: S32, month: U8, day: U8): DateError ! Date {
if month < 1 or month > 12 then { error DateError.new("there's no month " ++ month.toString() ++ " - months go from 1 to 12") }
if day < 1 or day > Date.lengthOf(year as S64, month) then { error DateError.new(year.toString() ++ "-" ++ month.toString() ++ " has no day " ++ day.toString()) }
return Date{epochDay := Instant.daysFromCivil(year as S64, month, day)}
}local function isLeap for Date(year: S64): Boolean {
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
}local function lengthOf for Date(year: S64, month: U8): U8 {
if month == 2 then {
return Date.isLeap(year) ? 29 : 28
}
return month == 4 or month == 6 or month == 9 or month == 11 ? 30 : 31
}global function fromDays for Date(days: S64): Date {
return Date{epochDay := days}
}global function today for Date(): Date {
return &clock.now().toDate(&timeZone)
}global method year for Date(self: Date): S32 {
let (year, _, _) := Instant.civilFromDays(self.epochDay)
return year as S32
}global method month for Date(self: Date): U8 {
let (_, month, _) := Instant.civilFromDays(self.epochDay)
return month
}global method day for Date(self: Date): U8 {
let (_, _, day) := Instant.civilFromDays(self.epochDay)
return day
}/// 1970-01-01 was a Thursday
global method weekday for Date(self: Date): Weekday {
return [Weekday.Monday, Weekday.Tuesday, Weekday.Wednesday, Weekday.Thursday, Weekday.Friday, Weekday.Saturday, Weekday.Sunday][(self.epochDay + 3).modulo(7) as U64]!!
}global method dayOfYear for Date(self: Date): U16 {
return (self.epochDay - Instant.daysFromCivil(self.year() as S64, 1, 1) + 1) as U16
}global method isLeapYear for Date(self: Date): Boolean {
return Date.isLeap(self.year() as S64)
}global method daysInMonth for Date(self: Date): U8 {
return Date.lengthOf(self.year() as S64, self.month())
}global method addDays for Date(self: Date, days: S64): Date {
return Date{epochDay := self.epochDay + days}
}/// the day kept when the month has it, its last day otherwise
global method addMonths for Date(self: Date, months: S64): Date {
let total := (self.year() as S64) * 12 + (self.month() as S64) - 1 + months
let year := total // 12
let month := (total.modulo(12) + 1) as U8
return Date{epochDay := Instant.daysFromCivil(year, month, self.day().min(Date.lengthOf(year, month)))}
}global method addYears for Date(self: Date, years: S64): Date {
return self.addMonths(years * 12)
}global method daysUntil for Date(self: Date, later: Date): S64 {
return later.epochDay - self.epochDay
}global method toDays for Date(self: Date): S64 {
return self.epochDay
}global method startOfMonth for Date(self: Date): Date {
return self.addDays(1 - (self.day() as S64))
}global method endOfMonth for Date(self: Date): Date {
return self.addDays((self.daysInMonth() - self.day()) as S64)
}global method at for Date(self: Date, time: TimeOfDay): DateTime {
return DateTime{date := self, time}
}global method format for Date(self: Date, pattern: String): String {
return pattern.replaceAll("YYYY", self.year().toString().padStart(4, "0")).replaceAll("MM", self.month().toString().padStart(2, "0")).replaceAll("DD", self.day().toString().padStart(2, "0"))
}#equatable-for-datemethod equals(self: Date, other: Date): Boolean {
return self.epochDay == other.epochDay
}#hashable-for-datemethod hash(self: Date): U64 {
return self.epochDay.hash()
}#comparable-for-datemethod compare(self: Date, other: Date): Ordering {
return self.epochDay.compare(other.epochDay)
}#tostring-for-datemethod toString(self: Date): String {
return self.format("YYYY-MM-DD")
}#fromstring-for-datefunction parse(text: String): ParseError ! Date {
let found := #regex"^(\d{4})-(\d{2})-(\d{2})quot;.match(text.trim())
if found == undefined then { error ParseError.new("not a date (YYYY-MM-DD): " ++ text) }
let number := λ(group: U64) U8.parse(found.group(group) ?? "") !? 0
return Date.of(S32.parse(found.group(1) ?? "") !? 0, number(2), number(3)) !? error ParseError.new("not a date (YYYY-MM-DD): " ++ text)
}#steppable-for-datemethod next(self: Date): Date {
return self.addDays(1)
}method previous(self: Date): Date {
return self.addDays(-1)
}#copy-for-dateNothing to write - a marker interface, or one another block already covers.
#timeofdayhow long after midnight
Layout
local const perDay: S128 := 86_400_000_000_000global function of for TimeOfDay(hour: U8, minute: U8, second: U8 := 0, nanosecond: U32 := 0): DateError ! TimeOfDay {
if hour > 23 or minute > 59 or second > 59 or nanosecond > 999_999_999 then { error DateError.new("there's no time " ++ hour.toString() ++ ":" ++ minute.toString() ++ ":" ++ second.toString() ++ " on a clock") }
return TimeOfDay{sinceMidnight := Duration.hours(hour as F64) + Duration.minutes(minute as F64) + Duration.seconds(second as F64) + Duration.nanoseconds(nanosecond as S64)}
}global function midnight for TimeOfDay(): TimeOfDay {
return TimeOfDay{sinceMidnight := Duration.zero()}
}global function noon for TimeOfDay(): TimeOfDay {
return TimeOfDay{sinceMidnight := Duration.hours(12.0)}
}global method hour for TimeOfDay(self: TimeOfDay): U8 {
return (self.sinceMidnight.toNanoseconds() / 3_600_000_000_000) as U8
}global method minute for TimeOfDay(self: TimeOfDay): U8 {
return (self.sinceMidnight.toNanoseconds() / 60_000_000_000 % 60) as U8
}global method second for TimeOfDay(self: TimeOfDay): U8 {
return (self.sinceMidnight.toNanoseconds() / 1_000_000_000 % 60) as U8
}global method nanosecond for TimeOfDay(self: TimeOfDay): U32 {
return (self.sinceMidnight.toNanoseconds() % 1_000_000_000) as U32
}global method sinceMidnight for TimeOfDay(self: TimeOfDay): Duration {
return self.sinceMidnight
}/// wraps past midnight
global method add for TimeOfDay(self: TimeOfDay, duration: Duration): TimeOfDay {
return TimeOfDay{sinceMidnight := Duration.nanoseconds((self.sinceMidnight.toNanoseconds() + duration.toNanoseconds()).modulo(perDay) as S64)}
}global method format for TimeOfDay(self: TimeOfDay, pattern: String): String {
let two := λ(n: U8) n.toString().padStart(2, "0")
let milliseconds := (self.nanosecond() / 1_000_000).toString().padStart(3, "0")
return pattern.replaceAll("hh", two(self.hour())).replaceAll("mm", two(self.minute())).replaceAll("ss", two(self.second())).replaceAll("SSS", milliseconds)
}#equatable-for-timeofdaymethod equals(self: TimeOfDay, other: TimeOfDay): Boolean {
return self.sinceMidnight == other.sinceMidnight
}#hashable-for-timeofdaymethod hash(self: TimeOfDay): U64 {
return self.sinceMidnight.hash()
}#comparable-for-timeofdaymethod compare(self: TimeOfDay, other: TimeOfDay): Ordering {
return self.sinceMidnight.compare(other.sinceMidnight)
}#tostring-for-timeofday"09:30:00", with as many fraction digits as it needs - "09:30:00.25"
method toString(self: TimeOfDay): String {
let fraction := self.nanosecond()
let digits := fraction.toString().padStart(9, "0").trimEnd("0")
return self.format("hh:mm:ss") ++ (fraction == 0 ? "" : "." ++ digits)
}#fromstring-for-timeofday"09:30", "09:30:15", "09:30:15.250"
function parse(text: String): ParseError ! TimeOfDay {
let found := #regex"^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?quot;.match(text.trim())
if found == undefined then { error ParseError.new("not a time of day (hh:mm, hh:mm:ss): " ++ text) }
let number := λ(group: U64) U8.parse(found.group(group) ?? "0") !? 0
let nanosecond := U32.parse((found.group(4) ?? "").padEnd(9, "0")) !? 0
return TimeOfDay.of(number(1), number(2), number(3), nanosecond) !? error ParseError.new("not a time of day: " ++ text)
}#copy-for-timeofdayNothing to write - a marker interface, or one another block already covers.
#datetimea date and a time of day, on a wall clock
Layout
global function of for DateTime(date: Date, time: TimeOfDay): DateTime {
return DateTime{date, time}
}global function now for DateTime(): DateTime {
return &clock.now().toDateTime(&timeZone)
}global method date for DateTime(self: DateTime): Date {
return self.date
}global method time for DateTime(self: DateTime): TimeOfDay {
return self.time
}/// nanoseconds since 1970-01-01T00:00 on the same wall clock - what add and since count in
local method wallNanos for DateTime(self: DateTime): S128 {
return (self.date.toDays() as S128) * perDay + self.time.sinceMidnight().toNanoseconds()
}local function fromWall for DateTime(nanos: S128): DateTime {
return DateTime{date := Date.fromDays((nanos // perDay) as S64), time := TimeOfDay{sinceMidnight := Duration.nanoseconds(nanos.modulo(perDay) as S64)}}
}global method add for DateTime(self: DateTime, duration: Duration): DateTime {
return DateTime.fromWall(self.wallNanos() + duration.toNanoseconds())
}global method subtract for DateTime(self: DateTime, duration: Duration): DateTime {
return DateTime.fromWall(self.wallNanos() - duration.toNanoseconds())
}global method since for DateTime(self: DateTime, earlier: DateTime): Duration {
return Duration.nanoseconds((self.wallNanos() - earlier.wallNanos()) as S64)
}global method addDays for DateTime(self: DateTime, days: S64): DateTime {
return DateTime{date := self.date.addDays(days), time := self.time}
}global method addMonths for DateTime(self: DateTime, months: S64): DateTime {
return DateTime{date := self.date.addMonths(months), time := self.time}
}/// the offsets a day before and a day after are the two it can be in: each that gives the wall clock
/// back is a reading of it - two when clocks were turned back, and the earlier wins; with none, clocks
/// jumped over it, and the offset from before the jump moves it past the jump
global method toInstant for DateTime(self: DateTime, zone: TimeZone := &timeZone): Instant {
let wall := Instant{sinceEpoch := Duration.nanoseconds(self.wallNanos() as S64)}
let early := wall - zone.offsetAt(wall - Duration.days(1.0))
let late := wall - zone.offsetAt(wall + Duration.days(1.0))
let fits := λ(at: Instant) at + zone.offsetAt(at) == wall
if fits(early) and fits(late) then {
return early.isBefore(late) ? early : late
}
return fits(late) and not fits(early) ? late : early
}global method format for DateTime(self: DateTime, pattern: String): String {
return self.time.format(self.date.format(pattern))
}#equatable-for-datetimemethod equals(self: DateTime, other: DateTime): Boolean {
return self.date == other.date and self.time == other.time
}#hashable-for-datetimemethod hash(self: DateTime): U64 {
return self.wallNanos().hash()
}#comparable-for-datetimemethod compare(self: DateTime, other: DateTime): Ordering {
return self.date.compare(other.date).then(self.time.compare(other.time))
}#tostring-for-datetimemethod toString(self: DateTime): String {
return self.date.toString() ++ "T" ++ self.time.toString()
}#fromstring-for-datetime"2026-10-07T09:30:00", "2026-10-07 09:30"
function parse(text: String): ParseError ! DateTime {
let t := text.trim()
let separator := t.slice(10..<11) !? ""
if separator != "T" and separator != " " then { error ParseError.new("not a date and time (YYYY-MM-DDThh:mm:ss): " ++ text) }
return DateTime{date := Date.parse(t.slice(0..<10) !? "")!!, time := TimeOfDay.parse(t.slice(11..<t.size()) !? "")!!}
}#copy-for-datetimeNothing to write - a marker interface, or one another block already covers.
#weekdayglobal method number for Weekday(self: Weekday): U8 {
return (self.index() + 1) as U8
}#equatable-for-weekdaymethod equals(self: Weekday, other: Weekday): Boolean {
return self.index() == other.index()
}#hashable-for-weekdaymethod hash(self: Weekday): U64 {
return self.index().hash()
}#comparable-for-weekdaymethod compare(self: Weekday, other: Weekday): Ordering {
return self.index().compare(other.index())
}#tostring-for-weekdayits name alone
method toString(self: Weekday): String {
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][self.index()]!!
}#copy-for-weekdayNothing to write - a marker interface, or one another block already covers.
#timezonea fixed offset, or a named zone whose offset at each instant the host knows - a JavaScript host knows every IANA zone (see JavaScript and TypeScript), a WASI runtime none
Layout
@js("host:time", "offset")
local function hostOffset for TimeZone(zone: String, unixMilliseconds: F64): Error ! F64@js("host:time", "local")
local function hostZone for TimeZone(): Error ! String/// "+02:00", "-05:30"
local function offsetText for TimeZone(offset: Duration): String {
let minutes := offset.toMinutes().round() as S64
return (minutes < 0 ? "-" : "+") ++ (minutes.abs() / 60).toString().padStart(2, "0") ++ ":" ++ (minutes.abs() % 60).toString().padStart(2, "0")
}global function utc for TimeZone(): TimeZone {
return TimeZone{label := "UTC", fixedOffset := Duration.zero()}
}global function fixed for TimeZone(offset: Duration): TimeZone {
return TimeZone{label := TimeZone.offsetText(offset), fixedOffset := offset}
}global function named for TimeZone(name: String): DateError ! TimeZone {
if name == "UTC" then {
return TimeZone.utc()
}
match TimeZone.hostOffset(name, 0.0) {
case Error(_) then { error DateError.new("unknown time zone '" ++ name ++ "'") }
case _ then { return TimeZone{label := name, fixedOffset := undefined} }
}
}global function host for TimeZone(): TimeZone {
return TimeZone.named(TimeZone.hostZone() !? "UTC") !? TimeZone.utc()
}global method name for TimeZone(self: TimeZone): String {
return self.label
}global method offsetAt for TimeZone(self: TimeZone, instant: Instant): Duration {
return self.fixedOffset ?? Duration.minutes(TimeZone.hostOffset(self.label, instant.toUnixMilliseconds() as F64) !? 0.0)
}global method isFixed for TimeZone(self: TimeZone): Boolean {
return self.fixedOffset != undefined
}#equatable-for-timezonemethod equals(self: TimeZone, other: TimeZone): Boolean {
return self.label == other.label
}#hashable-for-timezonemethod hash(self: TimeZone): U64 {
return self.label.hash()
}#tostring-for-timezonemethod toString(self: TimeZone): String {
return self.label
}#fromstring-for-timezone"UTC", "Z", "+02:00", "-0530", "+02", or a zone's name
function parse(text: String): ParseError ! TimeZone {
let t := text.trim()
if t == "UTC" or t == "Z" then {
return TimeZone.utc()
}
let found := #regex"^([+-])(\d{2}):?(\d{2})?quot;.match(t)
if found != undefined then {
let minutes := (S64.parse(found.group(2) ?? "0") !? 0) * 60 + (S64.parse(found.group(3) ?? "0") !? 0)
return TimeZone.fixed(Duration.minutes((found.group(1) == "-" ? -minutes : minutes) as F64))
}
return TimeZone.named(t) !? error ParseError.new("not a time zone: " ++ text)
}#copy-for-timezoneNothing to write - a marker interface, or one another block already covers.
#urltaken apart once, when it's parsed
Layout
/// RFC 3986's appendix B, as a pattern - scheme, authority, path, query, fragment
local const urlPattern: Regex := #regex"^([A-Za-z][A-Za-z0-9+.-]*):(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?quot;/// "a/./b/../c" is "a/c" - RFC 3986's remove_dot_segments
local function removeDots for Url(path: String): String {
let out: Array<String> := []
let segments := path.split("/")
for (i, segment) in segments.enumerate() {
let last := i + 1 == segments.size()
if segment == "." or segment == ".." then {
if segment == ".." and (out.size() > 1 or (out.size() == 1 and out[0] != "")) then {
out.pop()
}
if last then {
out.push("")
}
} else {
out.push(segment)
}
}
let joined := out.join("/")
return path.startsWith("/") and not joined.startsWith("/") ? "/" ++ joined : joined
}global function encode for Url(text: String): String {
return text.encode().toArray().map(λ(byte) {
let c := Codepoint.fromInteger(byte as U32) !? Codepoint.fromInteger(0)!!
return byte < 128 and (c.isAlphanumeric() or "-._~".contains(c)) ? c.toString() : "%" ++ byte.toString(16).toUpper().padStart(2, "0")
}).join("")
} uses #builtin.forever
global function decode for Url(text: String): ParseError ! String {
let bytes: Array<U8> := []
let at := 0
for _ in #builtin.forever() while at < text.size() {
let c := text.slice(at..<at + 1) !? ""
if c == "%" then {
bytes.push(U8.parse(text.slice(at + 1..<at + 3) !? "", 16) !? error ParseError.new("a '%' needs two hex digits after it: " ++ text))
set at := at + 3
} else {
bytes.pushAll(c.encode().toArray())
set at := at + 1
}
}
return Bytes.fromArray(bytes).decode<UTF8>()!!
}global method scheme for Url(self: Url): String {
return self.urlScheme
}global method user for Url(self: Url): String? {
return self.urlUser
}global method host for Url(self: Url): String {
return self.urlHost
}global method port for Url(self: Url): U16? {
return self.urlPort
}global method path for Url(self: Url): String {
return self.urlPath
}global method query for Url(self: Url): Map<String, String> {
let out: Map<String, String> := Map.of()
for pair in (self.urlQuery ?? "").split("&") if pair != "" {
let parts := pair.split("=")
let decoded := λ(text: String) Url.decode(text.replaceAll("+", " ")) !? text
out.set(decoded(parts[0] ?? ""), decoded(parts.slice(1..<parts.size()).join("=")))
}
return out
}global method fragment for Url(self: Url): String? {
return self.urlFragment
}global method origin for Url(self: Url): String {
return self.urlScheme ++ "://" ++ self.urlHost ++ (self.urlPort == undefined ? "" : ":" ++ self.urlPort.toString())
}/// RFC 3986's reference resolution
global method resolve for Url(self: Url, reference: String): ParseError ! Url {
let r := reference.trim()
if #regex"^[A-Za-z][A-Za-z0-9+.-]*:".test(r) then {
let absolute := Url.parse(r)!!
return absolute.withPath(Url.removeDots(absolute.urlPath))
}
if r.startsWith("//") then {
return Url.parse(self.urlScheme ++ ":" ++ r)!!
}
let found := #regex"^([^?#]*)(?:\?([^#]*))?(?:#(.*))?quot;.match(r)!!
let path := found.group(1) ?? ""
let query := found.group(2)
if path == "" then {
set path := self.urlPath
set query := query ?? self.urlQuery
} else if not path.startsWith("/") then {
let cut := self.urlPath.lastIndexOf("/")
set path := (cut == undefined ? "/" : self.urlPath.slice(0..<cut + 1) !? "/") ++ path
}
return Url{urlScheme := self.urlScheme, urlUser := self.urlUser, urlHost := self.urlHost, urlPort := self.urlPort, urlPath := Url.removeDots(path), urlQuery := query, urlFragment := found.group(3)}
}global method withPath for Url(self: Url, path: String): Url {
let rooted := path.startsWith("/") or self.urlHost == "" ? path : "/" ++ path
return Url{urlScheme := self.urlScheme, urlUser := self.urlUser, urlHost := self.urlHost, urlPort := self.urlPort, urlPath := rooted, urlQuery := self.urlQuery, urlFragment := self.urlFragment}
}/// replaced where it is, added at the end otherwise
global method withQuery for Url(self: Url, name: String, value: String): Url {
let written := Url.encode(name) ++ "=" ++ Url.encode(value)
let pairs: Array<String> := []
let replaced := false
for pair in (self.urlQuery ?? "").split("&") if pair != "" {
let key := Url.decode((pair.split("=")[0] ?? "").replaceAll("+", " ")) !? pair
if key != name then {
pairs.push(pair)
} else if not replaced then {
pairs.push(written)
set replaced := true
}
}
if not replaced then {
pairs.push(written)
}
return Url{urlScheme := self.urlScheme, urlUser := self.urlUser, urlHost := self.urlHost, urlPort := self.urlPort, urlPath := self.urlPath, urlQuery := pairs.join("&"), urlFragment := self.urlFragment}
}#equatable-for-urlmethod equals(self: Url, other: Url): Boolean {
return self.toString() == other.toString()
}#hashable-for-urlmethod hash(self: Url): U64 {
return self.toString().hash()
}#tostring-for-urlmethod toString(self: Url): String {
let authority := self.urlHost == "" and self.urlUser == undefined ? "" : "//" ++ (self.urlUser == undefined ? "" : self.urlUser ++ "@") ++ self.urlHost ++ (self.urlPort == undefined ? "" : ":" ++ self.urlPort.toString())
return self.urlScheme ++ ":" ++ authority ++ self.urlPath ++ (self.urlQuery == undefined ? "" : "?" ++ self.urlQuery) ++ (self.urlFragment == undefined ? "" : "#" ++ self.urlFragment)
}#fromstring-for-urlthe scheme and host in lower case - an IPv6 host keeps its brackets
function parse(text: String): ParseError ! Url {
let found := urlPattern.match(text.trim())
if found == undefined then { error ParseError.new("not a URL - it needs a scheme, like https: - " ++ text) }
let authority := found.group(2)
let user: String? := undefined
let host := ""
let port: U16? := undefined
if authority != undefined then {
let parts := #regex"^(?:([^@]*)@)?(\[[^\]]*\]|[^:]*)(?::(\d*))?quot;.match(authority)
if parts == undefined then { error ParseError.new("not a URL - its host can't be read: " ++ text) }
set user := parts.group(1)
set host := (parts.group(2) ?? "").toLower()
let digits := parts.group(3) ?? ""
set port := digits == "" ? undefined : U16.parse(digits) !? error ParseError.new("not a URL - its port isn't a number from 0 to 65535: " ++ text)
}
let path := found.group(3) ?? ""
return Url{urlScheme := (found.group(1) ?? "").toLower(), urlUser := user, urlHost := host, urlPort := port, urlPath := host != "" and path == "" ? "/" : path, urlQuery := found.group(4), urlFragment := found.group(5)}
}#copy-for-urlNothing to write - a marker interface, or one another block already covers.
#pathits segments, none empty or "."
Layout
global function of for Path(text: String): Path {
let t := text.replaceAll("\\", "/")
return Path{rooted := t.startsWith("/"), parts := t.split("/").filter(λ(part) part != "" and part != ".")}
}global method join for Path(self: Path, child: String): Path {
let other := Path.of(child)
return other.rooted ? other : Path{rooted := self.rooted, parts := self.parts ++ other.parts}
}global method parent for Path(self: Path): Path? {
if self.parts.isEmpty() then {
return undefined
}
return Path{rooted := self.rooted, parts := self.parts.slice(0..<self.parts.size() - 1)}
}global method name for Path(self: Path): String {
return self.parts.last() ?? ""
}/// where the extension starts - a "." at the very start (".gitignore") isn't one
local method dot for Path(self: Path): U64? {
let dot := self.name().lastIndexOf(".")
return dot == 0 ? undefined : dot
}global method stem for Path(self: Path): String {
let dot := self.dot()
return dot == undefined ? self.name() : self.name().slice(0..<dot) !? self.name()
}global method extension for Path(self: Path): String? {
let dot := self.dot()
return dot == undefined ? undefined : self.name().slice(dot + 1..<self.name().size()) !? undefined
}global method withName for Path(self: Path, name: String): Path {
return Path{rooted := self.rooted, parts := self.parts.slice(0..<self.parts.size().max(1) - 1) ++ [name]}
}global method withExtension for Path(self: Path, extension: String): Path {
let bare := extension.stripPrefix(".") ?? extension
return self.withName(bare == "" ? self.stem() : self.stem() ++ "." ++ bare)
}global method segments for Path(self: Path): Array<String> {
return self.parts.clone()
}global method isAbsolute for Path(self: Path): Boolean {
return self.rooted
}/// a ".." with nothing before it stays on a relative path, and goes on a rooted one
global method normalize for Path(self: Path): Path {
let out: Array<String> := []
for part in self.parts {
if part == ".." and out.last() != undefined and out.last() != ".." then {
out.pop()
} else if part != ".." or not self.rooted then {
out.push(part)
}
}
return Path{rooted := self.rooted, parts := out}
} uses #builtin.forever
global method relativeTo for Path(self: Path, base: Path): Path {
let from := base.normalize().parts
let to := self.normalize().parts
let same := 0
for _ in #builtin.forever() while same < from.size() and same < to.size() and from[same] == to[same] {
set same := same + 1
}
return Path{rooted := false, parts := Array.filled(from.size() - same, "..") ++ to.slice(same..<to.size())}
}global method startsWith for Path(self: Path, prefix: Path): Boolean {
return self.rooted == prefix.rooted and prefix.parts.size() <= self.parts.size() and prefix.parts.enumerate().all(λ((i, part)) self.parts[i] == part)
}global method matchesGlob for Path(self: Path, pattern: String): Boolean {
return expandBraces(pattern).any(λ(expanded) segmentsMatch(Path.of(expanded).parts, self.parts))
}/// a pattern with its {a,b,...} groups written out - one pattern per choice
local function expandBraces for Path(pattern: String): Array<String> {
let group := pattern.matches(#regex"\{([^{}]*)\}")[0] ?? (return [pattern])
let before := pattern.slice(0..<group.start())!!
let after := pattern.slice(group.end()..<pattern.size())!!
return group.group(1)!!.split(",").flatMap(λ(choice) expandBraces(before ++ choice ++ after)).toArray()
}/// the pattern's segments against the path's - ** takes any number of whole segments, none too
local function segmentsMatch for Path(pattern: Array<String>, path: Array<String>): Boolean {
if pattern.isEmpty() then {
return path.isEmpty()
}
if pattern[0] == "**" then {
return (0..=path.size()).any(λ(skip) segmentsMatch(pattern.drop(1), path.drop(skip)))
}
return not path.isEmpty() and segmentMatches(pattern[0]!!, path[0]!!) and segmentsMatch(pattern.drop(1), path.drop(1))
}/// one segment against one name: * any run, ? any one codepoint, [abc] [a-z] [!abc] one of a set, \x x itself
local function segmentMatches for Path(pattern: String, name: String): Boolean {
let regex := Regex.new("^" ++ pattern.codepoints().map(λ(c) c == '*' ? ".*" : c == '?' ? "." : "[]!^".contains(c.toString()) ? c.toString().replace("!", "^") : Regex.escape(c.toString())).join("") ++ "quot;)!!
return regex.test(name)
}#equatable-for-pathmethod equals(self: Path, other: Path): Boolean {
return self.toString() == other.toString()
}#hashable-for-pathmethod hash(self: Path): U64 {
return self.toString().hash()
}#comparable-for-pathmethod compare(self: Path, other: Path): Ordering {
return self.toString().compare(other.toString())
}#tostring-for-pathmethod toString(self: Path): String {
let joined := self.parts.join("/")
return self.rooted ? "/" ++ joined : joined == "" ? "." : joined
}#fromstring-for-pathfunction parse(text: String): ParseError ! Path {
if text.trim() == "" then { error ParseError.new("an empty text isn't a path") }
return Path.of(text)
}#copy-for-pathNothing to write - a marker interface, or one another block already covers.
#uuidits 32 hex digits, lowercase
Layout
/// the version digit and RFC 9562's variant bits written into 32 hex digits
local function stamped for Uuid(hex: String, version: String): Uuid {
let variant := ["8", "9", "a", "b"][((U8.parse(hex.slice(16..<17) !? "0", 16) !? 0) & 3) as U64]!!
return Uuid{hex := (hex.slice(0..<12) !? "") ++ version ++ (hex.slice(13..<16) !? "") ++ variant ++ (hex.slice(17..<32) !? "")}
}local function randomHex for Uuid(): String {
return &random.nextU64().toString(16).padStart(16, "0")
}global function nil for Uuid(): Uuid {
return Uuid{hex := "".padStart(32, "0")}
}global function v4 for Uuid(): Uuid {
return Uuid.stamped(Uuid.randomHex() ++ Uuid.randomHex(), "4")
}/// 48 bits of Unix milliseconds first - so they sort by when they were made
global function v7 for Uuid(): Uuid {
let time := (&clock.now().toUnixMilliseconds() as U64).toString(16).padStart(12, "0")
return Uuid.stamped(time ++ (Uuid.randomHex() ++ Uuid.randomHex()).slice(0..<20) !? "", "7")
}global function fromBytes for Uuid(bytes: Bytes): ParseError ! Uuid {
if bytes.size() != 16 then { error ParseError.new("a UUID is 16 bytes, got " ++ bytes.size().toString()) }
return Uuid{hex := bytes.toHex()}
}global method bytes for Uuid(self: Uuid): Bytes {
return Bytes.fromHex(self.hex)!!
}global method version for Uuid(self: Uuid): U8 {
return U8.parse(self.hex.slice(12..<13) !? "0", 16) !? 0
}global method timestamp for Uuid(self: Uuid): Instant? {
if self.version() != 7 then {
return undefined
}
return Instant.fromUnixMilliseconds((U64.parse(self.hex.slice(0..<12) !? "0", 16) !? 0) as S64)
}#equatable-for-uuidmethod equals(self: Uuid, other: Uuid): Boolean {
return self.hex == other.hex
}#hashable-for-uuidmethod hash(self: Uuid): U64 {
return self.hex.hash()
}#comparable-for-uuidmethod compare(self: Uuid, other: Uuid): Ordering {
return self.hex.compare(other.hex)
}#tostring-for-uuidmethod toString(self: Uuid): String {
let h := self.hex
return [h.slice(0..<8), h.slice(8..<12), h.slice(12..<16), h.slice(16..<20), h.slice(20..<32)].map(λ(part) part !? "").join("-")
}#fromstring-for-uuideither case, with or without its dashes or {braces}
function parse(text: String): ParseError ! Uuid {
let found := #regex"^\{?([0-9a-fA-F]{8})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{12})\}?quot;.match(text.trim())
if found == undefined then { error ParseError.new("not a UUID: " ++ text) }
return Uuid{hex := [1, 2, 3, 4, 5].map(λ(group) found.group(group) ?? "").join("").toLower()}
}#copy-for-uuidNothing to write - a marker interface, or one another block already covers.
#versionLayout
global function new for Version(major: U64, minor: U64 := 0, patch: U64 := 0): Version {
return Version{majorPart := major, minorPart := minor, patchPart := patch, pre := undefined, buildPart := undefined}
}global method major for Version(self: Version): U64 {
return self.majorPart
}global method minor for Version(self: Version): U64 {
return self.minorPart
}global method patch for Version(self: Version): U64 {
return self.patchPart
}global method preRelease for Version(self: Version): String? {
return self.pre
}global method build for Version(self: Version): String? {
return self.buildPart
}global method isPreRelease for Version(self: Version): Boolean {
return self.pre != undefined
}global method nextMajor for Version(self: Version): Version {
return Version.new(self.majorPart + 1)
}global method nextMinor for Version(self: Version): Version {
return Version.new(self.majorPart, self.minorPart + 1)
}global method nextPatch for Version(self: Version): Version {
return Version.new(self.majorPart, self.minorPart, self.patchPart + (self.isPreRelease() ? 0 : 1))
}/// npm's ranges: alternatives joined by ||, each a set of comparators every one of which must allow the
/// version - >=, <=, >, <, =, ^ (changes that keep the leftmost non-zero number), ~ (patches, or minors
/// when only a major is given), a partial version (1.2 is any 1.2.x), x / X / *, and a - b
global method satisfies for Version(self: Version, range: String): ParseError ! Boolean {
for alternative in range.split("||") {
let bounds := versionBounds(alternative.trim(), range)!!
let allowsPre := not self.isPreRelease() or bounds.any(λ(b) [b.low, b.high].any(λ(edge) edge?.isPreRelease() == true and (edge!!.majorPart, edge!!.minorPart, edge!!.patchPart) == (self.majorPart, self.minorPart, self.patchPart)))
if allowsPre and bounds.all(λ(b) b.allows(self)) then {
return true
}
}
return false
}local method allows for VersionBounds(self: VersionBounds, version: Version): Boolean {
let aboveLow := self.low == undefined or version > self.low!! or (version.compare(self.low!!) == Ordering.Equal and self.lowIncluded)
let belowHigh := self.high == undefined or version < self.high!! or (version.compare(self.high!!) == Ordering.Equal and self.highIncluded)
return aboveLow and belowHigh
}/// the numbers a partial version gives - up to three, stopping at x, X or * - and its pre-release when
/// all three are there
local function partial for Version(text: String, range: String): ParseError ! (Array<U64>, String?) {
let (numbers, pre) := text.stripPrefix("v")?.splitOnce("-") ?? (text.stripPrefix("v") ?? text, undefined)
let given := numbers.split("+")[0]!!.split(".").takeWhile(λ(part) part != "x" and part != "X" and part != "*" and part != "")
if given.size() > 3 then {
error ParseError.new("\"" ++ range ++ "\" isn't a version range - \"" ++ text ++ "\" has more than three numbers")
}
let parsed := given.map(λ(part) U64.parse(part) !? (error ParseError.new("\"" ++ range ++ "\" isn't a version - \"" ++ part ++ "\" isn't a number")))
return (parsed.toArray(), parsed.size() == 3 ? pre : undefined)
}/// the first version past every one a partial version stands for - 1.2 is 1.3.0
local function pastPartial for Version(numbers: Array<U64>): Version {
return numbers.size() <= 1 ? Version.new((numbers[0] ?? 0) + (numbers.isEmpty() ? 0 : 1)) : Version.new(numbers[0]!!, numbers[1]!! + 1)
}local function versionBounds for Version(text: String, range: String): ParseError ! Array<VersionBounds> {
let words := text.split(#regex"\s+").filter(λ(word) word != "")
let bounds: Array<VersionBounds> := []
let i: U64 := 0
for _ in forever() while i < words.size() {
if words[i + 1] == "-" and i + 2 < words.size() then {
let (from, fromPre) := partial(words[i]!!, range)!!
let (to, toPre) := partial(words[i + 2]!!, range)!!
let high := to.size() == 3 ? Version{majorPart := to[0]!!, minorPart := to[1]!!, patchPart := to[2]!!, pre := toPre, buildPart := undefined} : pastPartial(to)
bounds.push(VersionBounds{low := Version.new(from[0] ?? 0, from[1] ?? 0, from[2] ?? 0), high := to.isEmpty() ? undefined : high, lowIncluded := true, highIncluded := to.size() == 3})
set i += 3
continue _
}
/// an operator written apart from its version - ">= 1.2"
let word := [">=", "<=", ">", "<", "=", "^", "~"].contains(words[i]!!) ? words[i]!! ++ (words[i + 1] ?? "") : words[i]!!
set i += word == words[i] ? 1 : 2
let operator := [">=", "<=", ">", "<", "=", "^", "~"].find(λ(op) word.startsWith(op)) ?? ""
let (numbers, pre) := partial(word.stripPrefix(operator)!!, range)!!
let at := Version{majorPart := numbers[0] ?? 0, minorPart := numbers[1] ?? 0, patchPart := numbers[2] ?? 0, pre, buildPart := undefined}
let exact := numbers.size() == 3
bounds.push(match operator {
case ">=" then VersionBounds{low := at, high := undefined, lowIncluded := true, highIncluded := false}
case ">" then exact ? VersionBounds{low := at, high := undefined, lowIncluded := false, highIncluded := false} : VersionBounds{low := pastPartial(numbers), high := undefined, lowIncluded := true, highIncluded := false}
case "<" then VersionBounds{low := undefined, high := at, lowIncluded := false, highIncluded := false}
case "<=" then exact ? VersionBounds{low := undefined, high := at, lowIncluded := false, highIncluded := true} : VersionBounds{low := undefined, high := pastPartial(numbers), lowIncluded := false, highIncluded := false}
case "^" then {
let (major, minor) := (numbers[0] ?? 0, numbers[1] ?? 0)
let high := major > 0 or numbers.size() < 2 ? Version.new(major + 1) : minor > 0 or numbers.size() < 3 ? Version.new(0, minor + 1) : Version.new(0, 0, numbers[2]!! + 1)
VersionBounds{low := at, high, lowIncluded := true, highIncluded := false}
}
case "~" then VersionBounds{low := at, high := numbers.size() < 2 ? Version.new((numbers[0] ?? 0) + 1) : Version.new(numbers[0]!!, numbers[1]!! + 1), lowIncluded := true, highIncluded := false}
case _ then numbers.isEmpty() ? VersionBounds{low := undefined, high := undefined, lowIncluded := false, highIncluded := false} : exact ? VersionBounds{low := at, high := at, lowIncluded := true, highIncluded := true} : VersionBounds{low := Version.new(numbers[0]!!, numbers[1] ?? 0), high := pastPartial(numbers), lowIncluded := true, highIncluded := false}
})
}
return bounds
}/// release before pre-release is the wrong way round: 1.0.0-rc.1 < 1.0.0
local function preOrder for Version(a: String?, b: String?): Ordering {
if a == undefined or b == undefined then {
return a == b ? Ordering.Equal : a == undefined ? Ordering.Greater : Ordering.Less
}
for (x, y) in a.split(".").zip(b.split(".")) if x != y {
let (xNumber, yNumber) := (U64.parse(x), U64.parse(y))
if not (xNumber is Error) and not (yNumber is Error) then {
return xNumber!!.compare(yNumber!!)
}
if xNumber is Error != yNumber is Error then {
/// numeric identifiers come before the rest
return xNumber is Error ? Ordering.Greater : Ordering.Less
}
return x.compare(y)
}
return a.split(".").size().compare(b.split(".").size())
}#comparable-for-versionmethod compare(self: Version, other: Version): Ordering {
return self.majorPart.compare(other.majorPart).then(self.minorPart.compare(other.minorPart)).then(self.patchPart.compare(other.patchPart)).then(preOrder(self.pre, other.pre))
}#equatable-for-versionmethod equals(self: Version, other: Version): Boolean {
return self.compare(other) == Ordering.Equal and self.buildPart == other.buildPart
}#hashable-for-versionmethod hash(self: Version): U64 {
return self.toString().hash()
}#tostring-for-versionmethod toString(self: Version): String {
let text := #format"#{ self.majorPart }.#{ self.minorPart }.#{ self.patchPart }"
return text ++ (self.pre?.let(λ(p) "-" ++ p) ?? "") ++ (self.buildPart?.let(λ(b) "+" ++ b) ?? "")
}#fromstring-for-versionfunction parse(text: String): ParseError ! Version {
let found := text.trim().matches(#regex"^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?quot;)[0]
if found == undefined then {
error ParseError.new("\"" ++ text ++ "\" isn't a version - it's major.minor.patch, then -pre-release and +build if any")
}#copy-for-versionNothing to write - a marker interface, or one another block already covers.
#promise-typewhat an @async function compiles to: withResolver() makes its promise, the body runs as a task on the event loop, and what it returns settles the promise. await p parks the task and hands p.onSettle the rest of it, so the task carries on - with p's value - once p settles
Layout
uses #runtime.schedule
/// the first value stands - settling again does nothing, which is what lets race hand one lambda to all of them
local method settle for Promise<Type>(self: Promise<Type>, value: Type): Undefined {
if self.settled then { return undefined }
set self.settled := true
set self.value := value
for waiter in self.waiters {
#runtime.schedule(&clock.now(), λ waiter(value))
}
set self.waiters := []
} uses #runtime.schedule
/// queued rather than run right away, even when it's already settled - so code after an await always runs
/// in a later task, never in the middle of the caller's
local method onSettle for Promise<Type>(self: Promise<Type>, then: Lambda<Type, Undefined>): Undefined {
if self.settled then {
#runtime.schedule(&clock.now(), λ then(self.value))
} else {
self.waiters.push(then)
}
}global function resolved for Promise<Type>(value: Type): Promise<Type> {
return Promise<Type>{settled := true, value := value, waiters := []}
}global function withResolver for Promise<Type>(): (Promise<Type>, Lambda<Type, Undefined>) {
let promise := Promise<Type>{settled := false, value := undefined, waiters := []}
return (promise, λ(value) promise.settle(value))
}/// every promise is already running - awaiting them in turn only collects the values, it doesn't run
/// them one after another
@async
global function all for Promise<Type>(promises: Iterable<Promise<Type>>): Promise<Array<Type>> {
let values: Array<Type> := []
for promise in promises.iterator().collect() {
values.push(await promise)
}
return values
}global function race for Promise<Type>(promises: Iterable<Promise<Type>>): Promise<Type> {
let (first, settle) := Promise<Type>.withResolver()
for promise in promises {
promise.onSettle(settle)
}
return first
}/// the first value that isn't an error wins - settling again does nothing, so the rest are ignored; with
/// every one an error, the last to come is what's left
global function firstOk for Promise<Type>(promises: Iterable<Promise<Type>>): Promise<Type> {
let (first, settle) := Promise<Type>.withResolver()
let list := promises.iterator().collect()
let left := list.size()
for promise in list {
promise.onSettle(λ(value) {
set left := left - 1
if not value.is<Error>() or left == 0 then { settle(value) }
})
}
return first
}/// limit lanes, each starting the next item once its last one settles - @async, so every start sees the
/// caller's context values
@async
global function allLimited for Promise<Type, Item>(items: Iterable<Item>, limit: U64, start: Lambda<Item, Promise<Type>>): Promise<Array<Type>> {
let list := items.iterator().collect()
let values: Array<Type?> := Array.filled(list.size(), undefined)
let next := 0
@async
function lane(): Promise<Undefined> {
for _ in Iterator.forever() until next >= list.size() {
let i := next
set next := i + 1
set values[i] := await start(list[i])
}
}
await Promise.all((0..<(limit > 0 ? limit : 1)).map(λ(_) lane()))
return values.map(λ(value) value as Type)
}global function retry for Promise<Type>(attempts: U64, start: Lambda<(), Promise<Type>>, wait: Duration := Duration.zero()): Promise<Type> {
return retryAfter(attempts > 0 ? attempts : 1, start, wait)
} uses #runtime.onThread
/// on another thread when the host has one to spare, here and at once otherwise - the checker makes
/// sure the work is self-contained, so it gives the same either way (see @parallel)
global function parallel for Promise<Type>(work: Function, arguments: ..Unknown): Promise<Type> {
let started := #runtime.onThread(work, arguments)
if started = undefined then {
return Promise<Type>.resolved(work.call(arguments) as Type)
}
return started as Promise<Type>
}/// as parallel, with the caller's context values going along, and nothing about the work checked
global function parallelUnsafe for Promise<Type>(work: Function, arguments: ..Unknown): Promise<Type> {
let started := #runtime.onThreadUnsafe(work, arguments)
if started = undefined then {
return Promise<Type>.resolved(work.call(arguments) as Type)
}
return started as Promise<Type>
} uses #runtime.schedule
/// a FixedClock makes this instant: with nothing else due, the event loop sleeps the clock forward
/// early when &cancellation is cancelled, or at its deadline - settling twice settles once
global function sleep for Promise<Type>(duration: Duration): Promise<Undefined> {
let (done, settle) := Promise<Undefined>.withResolver()
let wake := &clock.now() + duration
#runtime.schedule(&cancellation.deadline != undefined and &cancellation.deadline.isBefore(wake) ? &cancellation.deadline : wake, λ settle(undefined))
&cancellation.onCancel(λ(_) settle(undefined))
return done
} uses #runtime.nextFrame
global function nextFrame for Promise<Type>(): Promise<F64> {
let (done, settle) := Promise<F64>.withResolver()
#runtime.nextFrame(λ(time) settle(time))
return done
}global method isSettled for Promise<Type>(self: Promise<Type>): Boolean {
return self.settled
}global method peek for Promise<Type>(self: Promise<Type>): Type? {
return self.settled ? self.value : undefined
}@async
global method map for Promise<Type, Result>(self: Promise<Type>, f: Lambda<Type, Result>): Promise<Result> {
return f(await self)
}@async
global method then for Promise<Type, Result>(self: Promise<Type>, f: Lambda<Type, Promise<Result>>): Promise<Result> {
return await f(await self)
}@async
global method zip for Promise<Type, Other>(self: Promise<Type>, other: Promise<Other>): Promise<(Type, Other)> {
return (await self, await other)
}@async
global method inspect for Promise<Type>(self: Promise<Type>, f: Lambda<Type, Undefined>): Promise<Type> {
let value := await self
f(value)
return value
}global method timeout for Promise<Type>(self: Promise<Type>, after: Duration): Promise<TimeoutError ! Type> {
let value: Promise<TimeoutError ! Type> := self.map(λ(value) value)
let expired: Promise<TimeoutError ! Type> := Promise.sleep(after).map(λ(_) {
let reason := &cancellation.reason()
return reason == undefined ? TimeoutError.new("nothing after " ++ after.toString()) : TimeoutError.new("cancelled before a result came", CancelledError.new(reason))
})
return Promise.race([value, expired])
} uses #runtime.runUntil
/// runs every task, not only this one's - inside an @async function, await instead: blocking there would
/// stop the very tasks it's waiting on
global method block for Promise<Type>(self: Promise<Type>): Type {
#runtime.runUntil(λ self.settled)
return self.value
}#tostring-for-promise-typemethod toString(self: Promise<Type>): String {
return self.settled ? "Promise(" ++ self.value.toString() ++ ")" : "Promise(pending)"
}#pointer-typea raw address - the memory operations are the machine's own
Layout
uses #builtin.addressOf
global function to for Pointer<Type>(value: Type): Pointer<Type> {
return Pointer<Type>{at := #builtin.addressOf(value)}
}global function allocate for Pointer<Type>(value: Type): OutOfMemoryError ! Pointer<Type> {
let pointer := Pointer<Type>.allocateMany(1)!!
pointer.write(value)
return pointer
} uses #builtin.allocate
/// room for count Types in a row on &heapAllocator, aligned for Type - not yet written
global function allocateMany for Pointer<Type>(count: U64): OutOfMemoryError ! Pointer<Type> {
return Pointer<Type>{at := #builtin.allocate(count * Type.size(), Type.size().max(1))!!}
} uses #builtin.load
global method read for Pointer<Type>(self: Pointer<Type>): Type {
return #builtin.load<Type>(self.at)
} uses #builtin.store
global method write for Pointer<Type>(self: Pointer<Type>, value: Type): Undefined {
#builtin.store(self.at, value)
}/// count Types further along - backward for a negative count
global method offset for Pointer<Type>(self: Pointer<Type>, count: S64): Pointer<Type> {
return Pointer<Type>{at := ((self.at as S64) + count * (Type.size() as S64)) as U64}
}global method address for Pointer<Type>(self: Pointer<Type>): U64 {
return self.at
}/// the same address, read as a different type - nothing is converted
global method cast for Pointer<Type, Target>(self: Pointer<Type>): Pointer<Target> {
return Pointer<Target>{at := self.at}
} uses #builtin.free
/// back to &heapAllocator - only for a pointer allocate / allocateMany gave
global method free for Pointer<Type>(self: Pointer<Type>): Undefined {
#builtin.free(self.at)
}#equatable-for-pointer-typemethod equals(self: Pointer<Type>, other: Pointer<Type>): Boolean {
return self.address() == other.address()
}#hashable-for-pointer-typemethod hash(self: Pointer<Type>): U64 {
return self.address().hash()
}#comparable-for-pointer-typemethod compare(self: Pointer<Type>, other: Pointer<Type>): Ordering {
return self.address().compare(other.address())
}#tostring-for-pointer-type/// "0x7ffd5e8c"
method toString(self: Pointer<Type>): String {
return "0x" ++ self.address().toString(16)
}#copy-for-pointer-typeNothing to write - a marker interface, or one another block already covers.
#rcpointer-typethe value and two counts in one heap block - the counts are what every owner and handle share
Layout
/// the compiler calls this as an owner goes out of scope (or is overwritten). The last owner drops the
/// value; the block itself stays until the last WeakPointer lets go too, so they can see it's gone
local method release for RcPointer<Type>(self: RcPointer<Type>): Undefined {
let (value, owners, weak) := self.box.read()
if owners == 1 and weak == 0 then {
self.box.free()
} else {
self.box.write((value, owners - 1, weak))
}
}global function new for RcPointer<Type>(value: Type): OutOfMemoryError ! RcPointer<Type> {
return RcPointer<Type>{box := Pointer.allocate((value, 1 as U64, 0 as U64))!!}
}global method read for RcPointer<Type>(self: RcPointer<Type>): Type {
return self.box.read()[0]
}global method write for RcPointer<Type>(self: RcPointer<Type>, value: Type): Undefined {
let (_, owners, weak) := self.box.read()
self.box.write((value, owners, weak))
}global method count for RcPointer<Type>(self: RcPointer<Type>): U64 {
return self.box.read()[1]
}global method isUnique for RcPointer<Type>(self: RcPointer<Type>): Boolean {
return self.count() == 1
}/// the value is the block's first part, at the block's own address
global method pointer for RcPointer<Type>(self: RcPointer<Type>): Pointer<Type> {
return self.box.cast<Type>()
}global method weak for RcPointer<Type>(self: RcPointer<Type>): WeakPointer<Type> {
let (value, owners, weak) := self.box.read()
self.box.write((value, owners, weak + 1))
return WeakPointer<Type>{box := self.box}
}global method unwrap for RcPointer<Type>(self: RcPointer<Type>): Type? {
return self.isUnique() ? self.read() : undefined
}#equatable-for-rcpointer-typemethod equals(self: RcPointer<Type>, other: RcPointer<Type>): Boolean {
return self.box == other.box
}#hashable-for-rcpointer-typemethod hash(self: RcPointer<Type>): U64 {
return self.box.hash()
}#tostring-for-rcpointer-typemethod toString(self: RcPointer<Type>): String {
return "RcPointer(" ++ self.read().toString() ++ ")"
}#clone-for-rcpointer-type/// one more owner of the same value
method clone(self: RcPointer<Type>): RcPointer<Type> {
let (value, owners, weak) := self.box.read()
self.box.write((value, owners + 1, weak))
return RcPointer<Type>{box := self.box}
}#weakpointer-typeLayout
/// the compiler calls this as a handle goes out of scope - the last one frees the block, once no
/// owner is left either
local method release for WeakPointer<Type>(self: WeakPointer<Type>): Undefined {
let (value, owners, weak) := self.box.read()
if owners == 0 and weak == 1 then {
self.box.free()
} else {
self.box.write((value, owners, weak - 1))
}
}global method upgrade for WeakPointer<Type>(self: WeakPointer<Type>): RcPointer<Type>? {
let (value, owners, weak) := self.box.read()
if owners == 0 then { return undefined }
self.box.write((value, owners + 1, weak))
return RcPointer<Type>{box := self.box}
}global method isAlive for WeakPointer<Type>(self: WeakPointer<Type>): Boolean {
return self.count() > 0
}global method count for WeakPointer<Type>(self: WeakPointer<Type>): U64 {
return self.box.read()[1]
}#equatable-for-weakpointer-typemethod equals(self: WeakPointer<Type>, other: WeakPointer<Type>): Boolean {
return self.box == other.box
}#hashable-for-weakpointer-typemethod hash(self: WeakPointer<Type>): U64 {
return self.box.hash()
}#clone-for-weakpointer-type/// one more handle - counted, so the block outlives it
method clone(self: WeakPointer<Type>): WeakPointer<Type> {
let (value, owners, weak) := self.box.read()
self.box.write((value, owners, weak + 1))
return WeakPointer<Type>{box := self.box}
}#structevery struct value - the field access itself is the compiler's, the rest is built on it
uses #runtime.fieldsOf
/// name to value, in declaration order
global method fields for Struct(self: Struct): Map<String, Unknown> {
return #runtime.fieldsOf(self)
} uses #runtime.construct
/// a value of type from its fields by name - a missing or unknown field is an Error
local function construct for Struct(type: Type, values: Map<String, Unknown>): Error ! Unknown {
return #runtime.construct(type, values)
}global method fieldNames for Struct(self: Struct): Array<String> {
return self.fields().keys().collect()
}global method get for Struct(self: Struct, name: String): Unknown? {
return self.fields().get(name)
}global method with for Struct(self: Struct, changes: Map<String, Unknown>): Error ! Self {
let values := self.fields()
values.merge(changes, undefined)
return Self.construct(self.typeOf(), values)!! as Self
}global method merge for Struct<Other: Struct>(self: Struct, other: Other): Self && Other {
let values := self.fields()
values.merge(other.fields(), undefined)
return Self.construct(Self && Other, values) !? abort(Error.new("a merged struct has every field of both"))
}global method typeOf for Struct(self: Struct): Type {
return Type.of(self)
}#equatable-for-structwhat @derive(Equatable) writes - only when every field is Equatable
method equals(self: Struct, other: Self): Boolean {
return self.typeOf() == other.typeOf() and self.fields().all(λ(entry) other.get(entry[0]) == entry[1])
}#hashable-for-structwhat @derive(Hashable) writes - only when every field is Hashable
method hash(self: Struct): U64 {
return #runtime.fieldValuesOf(self).reduce(self.typeOf().hash(), λ(hash, value) hashCombine(hash, value.hash()))
}#tostring-for-structwhat @derive(ToString) writes - "Point{x := 1, y := 2}", the same way the value is written
method toString(self: Struct): String {
let parts := self.fields().entries().map(λ(entry) entry[0] ++ " := " ++ entry[1].toString())
return self.typeOf().name() ++ "{" ++ parts.join(", ") ++ "}"
}#clone-for-structwhat @derive(Clone) writes - each field cloned
method clone(self: Struct): Self {
return Self.construct(self.typeOf(), self.fields().mapValues(λ(value) value.clone())) !? self
}#copy-for-structonly when every field is Copy
Nothing to write - a marker interface, or one another block already covers.
#enumevery enum value - which case it is, and that case's payload, are the compiler's
uses #runtime.variantOf
global method variant for Enum(self: Enum): Type {
return #runtime.variantOf(self)
} uses #runtime.payloadOf
/// the case's payload, positionally - a struct-like case's fields in order, a unit case's empty
local method payload for Enum(self: Enum): Tuple {
return #runtime.payloadOf(self)
}global function variants for Enum(): Array<Type> {
return Self.variants()
}global function variantNames for Enum(): Array<String> {
return Self.variants().map(λ(v) v.name())
}global method variantName for Enum(self: Enum): String {
return self.variant().name()
}global method is for Enum<Variant>(self: Enum): Boolean {
return self.variant() == Variant
}global method as for Enum<Variant>(self: Enum): Variant? {
return self.is<Variant>() ? self as Variant : undefined
}global method index for Enum(self: Enum): U64 {
return Self.variants().indexOf(self.variant()) ?? 0
}#equatable-for-enumwhat @derive(Equatable) writes - only when every case's payload is Equatable
method equals(self: Enum, other: Self): Boolean {
return self.variant() == other.variant() and self.payload() == other.payload()
}#hashable-for-enumwhat @derive(Hashable) writes - only when every case's payload is Hashable
method hash(self: Enum): U64 {
return hashCombine(hashCombine(&hashSeed, self.index()), self.payload().hash())
}#tostring-for-enum"Shape.Circle(5)", or just "Shape.Empty" for a unit case
method toString(self: Enum): String {
let name := Type.of(self).name() ++ "." ++ self.variantName()
return self.payload().size() == 0 ? name : name ++ self.payload().toString()
}#clone-for-enummethod clone(self: Enum): Self {
return self
}#copy-for-enumonly when every case's payload is Copy
Nothing to write - a marker interface, or one another block already covers.
#tupleevery tuple value
uses #runtime.tupleGet
global method get for Tuple(self: Tuple, index: U64): Unknown? {
return #runtime.tupleGet(self, index)
}global method size for Tuple(self: Tuple): U64 {
return self.typeOf().fields().size()
}global method first for Tuple(self: Tuple): Unknown? {
return self.get(0)
}global method last for Tuple(self: Tuple): Unknown? {
return self.size() == 0 ? undefined : self.get(self.size() - 1)
}global method toArray for Tuple(self: Tuple): Array<Unknown> {
return Array.generate(self.size(), λ(i) self.get(i))
}global method typeOf for Tuple(self: Tuple): Type {
return Type.of(self)
}#equatable-for-tuplemethod equals(self: Tuple, other: Self): Boolean {
return self.toArray() == other.toArray()
}#hashable-for-tuplemethod hash(self: Tuple): U64 {
return self.toArray().hash()
}#comparable-for-tuplethe first differing field decides
method compare(self: Tuple, other: Self): Ordering {
return self.toArray().compare(other.toArray())
}#tostring-for-tuple"(1, "a")" - and "(1,)" for one field, the same way it's written
method toString(self: Tuple): String {
return "(" ++ self.toArray().join(", ") ++ (self.size() == 1 ? ",)" : ")")
}#concatenable-other-self-other-for-tuple-other-tuple(1, "a") ++ (true,) is (1, "a", true)
uses #runtime.tupleAppend
method concat(self: Tuple, other: Other): Self & Other {
return #runtime.tupleAppend(self, other) as Self & Other
}#clone-for-tuplemethod clone(self: Tuple): Self {
return self
}#copy-for-tupleonly when every field is Copy
Nothing to write - a marker interface, or one another block already covers.
#unionevery untagged union value - a value doesn't record which member it is beyond its own runtime type
global function members for Union(): Array<Type> {
return Self.variants()
}global method is for Union<Member>(self: Union): Boolean {
return Member.isAssignableFrom(self.typeOf())
}global method as for Union<Member>(self: Union): CastError ! Member {
if not self.is<Member>() then { error CastError.new("the value is a " ++ self.typeOf().name() ++ ", not a " ++ Member.name()) }
return self as Member
} uses #runtime.typeof
/// the member it holds right now
global method typeOf for Union(self: Union): Type {
return #runtime.typeof(self)
}#equatable-for-uniononly when every member is Equatable - different members are never equal
method equals(self: Union, other: Self): Boolean {
return self.typeOf() == other.typeOf() and (self as Equatable) == (other as Equatable)
}#hashable-for-unionmethod hash(self: Union): U64 {
return hashCombine(self.typeOf().hash(), (self as Hashable).hash())
}#tostring-for-unionmethod toString(self: Union): String {
return (self as ToString).toString()
}#wrappera single hidden field - a new name for Inner, sharing nothing else with it unless implemented below
Layout
global function wrap for Wrapper(value: Inner): Self {
return Self{inner := value}
}global method unwrap for Wrapper(self: Wrapper): Inner {
return self.inner
}global method map for Wrapper(self: Wrapper, f: Lambda<Inner, Inner>): Self {
return Self{inner := f(self.inner)}
}#equatable-for-wrapperopt-in, and only when Inner is Equatable
method equals(self: Wrapper, other: Self): Boolean {
return self.inner == other.inner
}#hashable-for-wrapperopt-in, and only when Inner is Hashable
method hash(self: Wrapper): U64 {
return self.inner.hash()
}#comparable-for-wrapperopt-in, and only when Inner is Comparable
method compare(self: Wrapper, other: Self): Ordering {
return self.inner.compare(other.inner)
}#tostring-for-wrapperopt-in, and only when Inner is ToString
method toString(self: Wrapper): String {
return self.inner.toString()
}#clone-for-wrapperopt-in, and only when Inner is Clone
method clone(self: Wrapper): Self {
return Self{inner := self.inner.clone()}
}#copy-for-wrapperopt-in, and only when Inner is Copy
Nothing to write - a marker interface, or one another block already covers.
#function-args-result-error-undefinedFunction, Method and Lambda share these - a call's argument list is a tuple, Args
global method arity for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): U64 {
return #comptime.functionArity(self)
}/// undefined for a Lambda
global method name for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): String? {
return #comptime.functionName(self)
} uses #comptime.functionSignature
global method signature for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): Type {
return #comptime.functionSignature(self)
} uses #comptime.partial
/// fixes the first argument - splitting Args needs its shape, which only the compiler knows
global method partial for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>, first: Head<Args>): Function<Tail<Args>, Result, Error> {
return #comptime.partial(self, first) as Function<Tail<Args>, Result, Error>
} uses #comptime.curry
global method curry for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): Function {
return #comptime.curry(self)
} uses #comptime.flip
global method flip for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): Function {
return #comptime.flip(self)
} uses #comptime.isPure
/// @pure
global method isPure for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): Boolean {
return #comptime.isPure(self)
}global method compose for Function<Args, Result, Error:=undefined, Next>(self: Function<Args, Result, Error>, next: Function<Result, Next>): Function<Args, Next> {
return λ(args) next.call(self.call(args))
}global method pipeInto for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>, value: Args): Error ! Result {
return self.call(value)
}/// results kept by argument list - Args: Hashable
global method memoize for Function<Args, Result, Error:=undefined>(self: Function<Args, Result, Error>): Function<Args, Result, Error> {
let cache: Map<Args, Result> := Map.of()
return λ(args) cache.getOrInsert(args, λ self.call(args))
}global function identity for Function<Args, Result, Error:=undefined, T>(): Function<T, T> {
return λ(x) x
}global function constant for Function<Args, Result, Error:=undefined, T>(value: T): Function<Unknown, T> {
return λ(_) value
}#callable-args-result-for-function-args-result-error-undefined uses #runtime.invoke
/// f(args)
method call(self: Function<Args, Result, Error>, args: Args): Error ! Result {
return #runtime.invoke(self, args)
}#equatable-for-function-args-result-error-undefined/// the same code - and for a Lambda the same captured values, for a Method the same self
method equals(self: Function<Args, Result, Error>, other: Function<Args, Result, Error>): Boolean {
return #runtime.sameFunction(self, other)
}#tostring-for-function-args-result-error-undefined/// "add(a: Number, b: Number): Number", or "λ(x: Number): Number" for a Lambda
method toString(self: Function<Args, Result, Error>): String {
return (self.name() ?? "λ") ++ self.signature().name()
}#copy-for-function-args-result-error-undefinedNothing to write - a marker interface, or one another block already covers.
#method-args-result-error-undefinedeverything on Function, bound to the value it was read from
uses #runtime.boundSelf
/// a in a.method
global method self for Method<Args, Result, Error:=undefined>(self: Method<Args, Result, Error>): Unknown {
return #runtime.boundSelf(self)
} uses #runtime.unbind
/// the same code, as a Function taking self as its first argument
global method unbind for Method<Args, Result, Error:=undefined>(self: Method<Args, Result, Error>): Function {
return #runtime.unbind(self)
}#lambda-args-result-error-undefinedeverything on Function, plus what it closed over
uses #runtime.captures
/// the captured variables, by name, as they are now
global method captures for Lambda<Args, Result, Error:=undefined>(self: Lambda<Args, Result, Error>): Map<String, Unknown> {
return #runtime.captures(self)
}#interfacean interface as a value - read from the compiler's list of interfaces and implement blocks
global method name for Interface(self: Interface): String {
return #comptime.interfaceName(self)
} uses #comptime.interfaceMembers
/// every Method / Function it requires, with its type
global method members for Interface(self: Interface): Map<String, Type> {
return #comptime.interfaceMembers(self)
}/// every type with an implement block for it in scope
global method implementors for Interface(self: Interface): Array<Type> {
return #comptime.implementors(self)
}global method isImplementedBy for Interface(self: Interface, type: Type): Boolean {
return type.implements(self)
}#equatable-for-interfacemethod equals(self: Interface, other: Interface): Boolean {
return #comptime.sameInterface(self, other)
}#hashable-for-interfacemethod hash(self: Interface): U64 {
return self.name().hash()
}#tostring-for-interfacemethod toString(self: Interface): String {
return self.name()
}#fixedclockLayout
global function new for FixedClock(at: Instant := Instant.epoch()): FixedClock {
return Self{current := at}
}global method set for FixedClock(self: FixedClock, at: Instant): Undefined {
set self.current := at
}global method advance for FixedClock(self: FixedClock, by: Duration): Undefined {
set self.current += by
}#clock-for-fixedclockmethod now(self: FixedClock): Instant {
return self.current
}/// returns at once - the time jumps ahead instead, so code that waits runs instantly
method sleep(self: FixedClock, duration: Duration): Undefined {
self.advance(duration)
}#offsetclockLayout
/// inner defaults to the clock in effect where it's made - so injecting one over &clock shifts the real time
global function new for OffsetClock(offset: Duration, inner: Clock := &clock): OffsetClock {
return Self{offset := offset, inner := inner}
}#clock-for-offsetclockmethod now(self: OffsetClock): Instant {
return self.inner.now() + self.offset
}method sleep(self: OffsetClock, duration: Duration): Undefined {
self.inner.sleep(duration)
}#seededrandomLayout
/// SplitMix64 - one U64 of state, stepped by a fixed odd constant and scrambled on the way out, so every
/// seed (0 included) gives a full-length sequence
global function new for SeededRandom(seed: U64): SeededRandom {
return Self{state := seed}
}#random-for-seededrandommethod nextU64(self: SeededRandom): U64 {
set self.state := self.state.wrappingAdd(0x9e3779b97f4a7c15)
let z := self.state
set z := (z bxor (z shr 30)).wrappingMultiply(0xbf58476d1ce4e5b9)
set z := (z bxor (z shr 27)).wrappingMultiply(0x94d049bb133111eb)
return z bxor (z shr 31)
}/// the top 53 bits over 2^53 - every result an exact F64, evenly spaced in [0, 1)
method nextF64(self: SeededRandom): F64 {
return ((self.nextU64() shr 11) as F64) / 9007199254740992.0
}/// draw % span on its own would favour the low end of the range whenever span doesn't divide 2^64 -
/// so the 2^64 % span draws that cause it are thrown away and drawn again. A range can hold at most
/// 2^64 values (only 128- and 256-bit ranges hold more), and must hold at least one
method between(self: SeededRandom, range: Range<Integer>): Integer {
let first := range.start() as S128
let last := (range.isInclusive() ? range.end() : range.end() - 1) as S128
if last < first then { return abort(EmptyError.new("between needs a range with at least one value")) }
let distance := last - first
if distance > (U64.max() as S128) then { return abort(OverflowError.new("between can't draw from more than 2^64 values")) }
if distance == (U64.max() as S128) then { return (first + (self.nextU64() as S128)) as Integer }
let span := (distance as U64) + 1
let threshold := U64.zero().wrappingSubtract(span) % span
let draw := self.nextU64()
for _ in forever() while draw < threshold {
set draw := self.nextU64()
}
return (first + ((draw % span) as S128)) as Integer
}#memoryoutputstreamLayout
global function new for MemoryOutputStream(): MemoryOutputStream {
return Self{buffer := []}
}global method bytes for MemoryOutputStream(self: MemoryOutputStream): Bytes {
return Bytes.fromArray(self.buffer)
}global method text for MemoryOutputStream(self: MemoryOutputStream): ParseError ! String {
return String.decode(self.bytes())
}global method clear for MemoryOutputStream(self: MemoryOutputStream): Undefined {
self.buffer.clear()
}#outputstream-for-memoryoutputstreammethod write(self: MemoryOutputStream, bytes: Bytes): IOError ! Undefined {
set self.buffer ++= bytes
}method writeText(self: MemoryOutputStream, text: String): IOError ! Undefined {
set self.buffer ++= text.encode()
}/// nothing is held back - every write is in the buffer already
method flush(self: MemoryOutputStream): IOError ! Undefined {
return undefined
}#nulloutputstreamglobal function new for NullOutputStream(): NullOutputStream {
return Self{}
}#outputstream-for-nulloutputstreammethod write(self: NullOutputStream, bytes: Bytes): IOError ! Undefined {
return undefined
}method writeText(self: NullOutputStream, text: String): IOError ! Undefined {
return undefined
}method flush(self: NullOutputStream): IOError ! Undefined {
return undefined
}#teeoutputstreamLayout
global function new for TeeOutputStream(first: OutputStream, second: OutputStream): TeeOutputStream {
return Self{first := first, second := second}
}#outputstream-for-teeoutputstreamthe first stream's error stops the write before the second - so a failure never leaves only the second stream written to
method write(self: TeeOutputStream, bytes: Bytes): IOError ! Undefined {
self.first.write(bytes)!!
self.second.write(bytes)!!
}method writeText(self: TeeOutputStream, text: String): IOError ! Undefined {
self.first.writeText(text)!!
self.second.writeText(text)!!
}method flush(self: TeeOutputStream): IOError ! Undefined {
self.first.flush()!!
self.second.flush()!!
}#memoryinputstreamLayout
global function fromBytes for MemoryInputStream(bytes: Bytes): MemoryInputStream {
return Self{data := bytes.clone(), position := 0}
}global function fromText for MemoryInputStream(text: String): MemoryInputStream {
return Self.fromBytes(text.encode())
}global method remaining for MemoryInputStream(self: MemoryInputStream): U64 {
return self.data.size() - self.position
}/// the next count bytes, or fewer at the end - moving past them
local method take for MemoryInputStream(self: MemoryInputStream, count: U64): Bytes {
let end := self.position + count.min(self.remaining())
let chunk := self.data.slice(self.position..<end) !? Bytes.filled(0)
set self.position := end
return chunk
}#inputstream-for-memoryinputstreammethod read(self: MemoryInputStream, count: U64): IOError ! Bytes {
return self.take(count)
}/// up to the next \n, without it or a \r right before it
method readLine(self: MemoryInputStream): IOError ! String? {
if self.remaining() == 0 then { return undefined }
let newline := self.data.indexOf(Bytes.filled(1, 10), self.position)
let line := self.take((newline ?? self.data.size()) - self.position)
if newline != undefined then { self.take(1) }
let text := line.endsWith(Bytes.filled(1, 13)) ? line.slice(0..<(line.size() - 1)) !? line : line
match String.decode(text) {
case Error(e) then { error IOError.new("line isn't valid UTF-8", e) }
case decoded then { return decoded }
}
}method readAll(self: MemoryInputStream): IOError ! Bytes {
return self.take(self.remaining())
}#memoryenvironmentLayout
global function new for MemoryEnvironment(variables: Map<String, String> := Map.of()): MemoryEnvironment {
return Self{variables := variables.clone()}
}#environment-for-memoryenvironmentmethod get(self: MemoryEnvironment, name: String): String? {
return self.variables.get(name)
}method set(self: MemoryEnvironment, name: String, value: String): Undefined {
self.variables.set(name, value)
}/// a copy - changing it doesn't change the environment
method all(self: MemoryEnvironment): Map<String, String> {
return self.variables.clone()
}#memoryfilesystemLayout
global function new for MemoryFileSystem(): MemoryFileSystem {
return Self{files := Map.of(), directories := Set.of()}
}global function of for MemoryFileSystem(files: Map<String, String>): MemoryFileSystem {
let fs := Self.new()
/// a path that runs through another file is left out - there's no error to return it with
for entry in files.entries() {
fs.writeText(entry[0], entry[1]) !? undefined
}
return fs
}/// the root as "" and no trailing "/" - so a path's parent is always everything before its last "/"
local function clean for MemoryFileSystem(path: String | Path): String {
let text := path.toString()
return text.stripSuffix("/") ?? text
}/// every directory above path but the root, nearest last - "/a/b/c" gives "/a", "/a/b"
local function parents for MemoryFileSystem(path: String | Path): Array<String> {
let key := Self.clean(path)
let result: Array<String> := []
let at := key.lastIndexOf("/")
for _ in forever() while (at ?? 0) > 0 {
let parent := key.slice(0..<(at ?? 0)) !? ""
result.push(parent)
set at := parent.lastIndexOf("/")
}
return result.reverse()
}local method notFound for MemoryFileSystem(self: MemoryFileSystem, path: String | Path): IOError {
return IOError.new("no such file or directory: " ++ path.toString())
}#filesystem-for-memoryfilesystemmethod read(self: MemoryFileSystem, path: String | Path): IOError ! Bytes {
let bytes := self.files.get(Self.clean(path)) ?! self.notFound(path)
return bytes!!.clone()
}method readText(self: MemoryFileSystem, path: String | Path): IOError ! String {
match String.decode(self.read(path)!!) {
case Error(e) then { error IOError.new("not valid UTF-8: " ++ path.toString(), e) }
case text then { return text }
}
}/// creates any missing directories above path, like mkdir -p
method write(self: MemoryFileSystem, path: String | Path, bytes: Bytes): IOError ! Undefined {
let key := Self.clean(path)
if key == "" or self.isDirectory(key) then { error IOError.new("is a directory: " ++ path.toString()) }
for parent in Self.parents(key) {
if self.files.has(parent) then { error IOError.new("not a directory: " ++ parent) }
self.directories.add(parent)
}
self.files.set(key, bytes.clone())
}method writeText(self: MemoryFileSystem, path: String | Path, text: String): IOError ! Undefined {
self.write(path, text.encode())!!
}/// sorted, so a test can compare it with a literal
method list(self: MemoryFileSystem, path: String | Path): IOError ! Array<String> {
let key := Self.clean(path)
if not self.isDirectory(key) then { error self.notFound(path) }
let prefix := key ++ "/"
let names: Set<String> := Set.of()
for entry in self.files.keys().chain(self.directories.iterator()) if entry.startsWith(prefix) {
let rest := entry.stripPrefix(prefix) ?? entry
names.add(rest.split("/").first() ?? rest)
}
return names.toArray().sorted()
}method exists(self: MemoryFileSystem, path: String | Path): Boolean {
return self.files.has(Self.clean(path)) or self.isDirectory(path)
}method isDirectory(self: MemoryFileSystem, path: String | Path): Boolean {
let key := Self.clean(path)
return key == "" or self.directories.has(key)
}method createDirectory(self: MemoryFileSystem, path: String | Path): IOError ! Undefined {
let key := Self.clean(path)
if self.files.has(key) then { error IOError.new("a file is in the way: " ++ path.toString()) }
for parent in Self.parents(key) {
if self.files.has(parent) then { error IOError.new("not a directory: " ++ parent) }
self.directories.add(parent)
}
if key != "" then { self.directories.add(key) }
}/// a file, or an empty directory - never the root
method remove(self: MemoryFileSystem, path: String | Path): IOError ! Undefined {
let key := Self.clean(path)
if self.files.remove(key) != undefined then { return undefined }
if key == "" or not self.directories.has(key) then { error self.notFound(path) }
if not self.list(key)!!.isEmpty() then { error IOError.new("directory isn't empty: " ++ path.toString()) }
self.directories.remove(key)
}#loggerwhat every Logger gets - each is log() at its own level, so an implementor only writes log()
global method debug for Logger(self: Logger, message: String, fields: Map<String, Unknown> := Map.of()): Undefined {
self.log(LogLevel.Debug, message, fields)
}global method info for Logger(self: Logger, message: String, fields: Map<String, Unknown> := Map.of()): Undefined {
self.log(LogLevel.Info, message, fields)
}global method warn for Logger(self: Logger, message: String, fields: Map<String, Unknown> := Map.of()): Undefined {
self.log(LogLevel.Warn, message, fields)
}global method error for Logger(self: Logger, message: String, fields: Map<String, Unknown> := Map.of()): Undefined {
self.log(LogLevel.Error, message, fields)
}#filesystemglobal method glob for FileSystem(self: FileSystem, pattern: String, from: String | Path := "."): IOError ! Array<String> {
let root := Path.of(from.toString())
let found: Set<String> := Set.of()
/// a directory's entries, and theirs - relative to root - when the pattern can still reach below it
let walk := λ(relative: Path, depth: U64) {
for name in self.list(root.join(relative.toString()))!! {
let path := relative.join(name)
if path.matchesGlob(pattern) then {
found.add(path.toString())
}
if self.isDirectory(root.join(path.toString())) and (pattern.contains("**") or depth + 1 < pattern.split("/").size()) then {
walk(path, depth + 1)!!
}
}
}
walk(Path.of(""), 0)!!
return found.toArray().sorted()
}#sqlitedatabaseLayout
/// the host's SQLite - a JavaScript host provides it (see JavaScript and TypeScript)
@js("host:sqlite", "open")
local function connect for SqliteDatabase(path: String): JsValue@js("host:sqlite", "query")
local function select for SqliteDatabase(connection: JsValue, sql: String, parameters: Array<Unknown>): Array<Map<String, Unknown>>@js("host:sqlite", "execute")
local function change for SqliteDatabase(connection: JsValue, sql: String, parameters: Array<Unknown>): U64/// a failed statement comes back from the host as an Error - the same failure, as an IOError
local function io for SqliteDatabase<T>(result: Error ! T): IOError ! T {
match result {
case Error(message) then { error IOError.new(message.toString()) }
case value then { return value }
}
}global function open for SqliteDatabase(path: String): IOError ! SqliteDatabase {
return Self{connection := Self.io(Self.connect(path))!!}
}global function memory for SqliteDatabase(): IOError ! SqliteDatabase {
return Self.open(":memory:")!!
}#database-for-sqlitedatabasemethod query(self: SqliteDatabase, sql: String, parameters: Array<Unknown>): IOError ! Array<Map<String, Unknown>> {
return Self.io(Self.select(self.connection, sql, parameters))!!
}method execute(self: SqliteDatabase, sql: String, parameters: Array<Unknown>): IOError ! U64 {
return Self.io(Self.change(self.connection, sql, parameters))!!
}#memoryhttpclientLayout
global function new for MemoryHttpClient(answers: Map<String, Response> := Map.of()): MemoryHttpClient {
return Self{answers := answers.clone(), sent := []}
}global method requests for MemoryHttpClient(self: MemoryHttpClient): Array<HttpRequest> {
return self.sent.clone()
}#httpclient-for-memoryhttpclient/// settled at once - the request is copied, so changing it afterwards doesn't change what was sent
method send(self: MemoryHttpClient, request: HttpRequest): Promise<IOError ! Response> {
self.sent.push(request.clone())
let answer := self.answers.get(request.method ++ " " ++ request.url) ?? Response.text("Not Found", 404)
return Promise.resolved(answer.clone())
}#filesnapshotsLayout
/// characters a file name can't hold become "_"
local method fileFor for FileSnapshots(self: FileSnapshots, name: String): String {
return self.folder ++ "/" ++ name.replaceAll(#regex"[/\\:*?\"<>|]", "_") ++ ".snap"
}/// the update flag from the process's own environment, not &env - a test may well replace that
global function new for FileSnapshots(folder: String := "snapshots"): FileSnapshots {
return FileSnapshots{folder, update := Builtin.defaultEnv().get("REFINGERE_UPDATE_SNAPSHOTS") == "1", unwritable := Map.of()}
}#snapshotstore-for-filesnapshotsthe program's own files, not &fs - a test may well replace that with files in memory
method read(self: FileSnapshots, name: String): IOError ! String? {
if self.unwritable.has(name) then {
return self.unwritable.get(name)
}
let files := Builtin.defaultFs()
let path := self.fileFor(name)
return files.exists(path) ? files.readText(path)!! : undefined
}/// a host whose files can't be written keeps the text for the run instead
method write(self: FileSnapshots, name: String, text: String): IOError ! Undefined {
let files := Builtin.defaultFs()
if not files.isDirectory(self.folder) and files.createDirectory(self.folder) is Error then {
self.unwritable.set(name, text)
} else if files.writeText(self.fileFor(name), text) is Error then {
self.unwritable.set(name, text)
}
}method updating(self: FileSnapshots): Boolean {
return self.update
}#memorysnapshotsLayout
global function new for MemorySnapshots(texts: Map<String, String> := Map.of(), update: Boolean := false): MemorySnapshots {
return MemorySnapshots{kept := texts.clone(), update}
}global method texts for MemorySnapshots(self: MemorySnapshots): Map<String, String> {
return self.kept.clone()
}#snapshotstore-for-memorysnapshotsmethod read(self: MemorySnapshots, name: String): IOError ! String? {
return self.kept.get(name)
}method write(self: MemorySnapshots, name: String, text: String): IOError ! Undefined {
self.kept.set(name, text)
}method updating(self: MemorySnapshots): Boolean {
return self.update
}#memoryloggerLayout
global function new for MemoryLogger(): MemoryLogger {
return Self{records := []}
}global method entries for MemoryLogger(self: MemoryLogger): Array<(level: LogLevel, message: String, fields: Map<String, Unknown>)> {
return self.records.clone()
}global method messages for MemoryLogger(self: MemoryLogger, level: LogLevel?): Array<String> {
return self.records.filter(λ(entry) level == undefined or entry.level == level).map(λ(entry) entry.message)
}global method clear for MemoryLogger(self: MemoryLogger): Undefined {
self.records.clear()
}#logger-for-memorylogger/// fields is copied - changing the Map afterwards doesn't change what was logged
method log(self: MemoryLogger, level: LogLevel, message: String, fields: Map<String, Unknown>): Undefined {
self.records.push((level := level, message := message, fields := fields.clone()))
}#streamloggerLayout
global function new for StreamLogger(stream: OutputStream := &stderr, minimum: LogLevel := LogLevel.Info): StreamLogger {
return Self{stream := stream, minimum := minimum}
}/// Debug < Info < Warn < Error - LogLevel is a plain union, so it has no order of its own
local function severity for StreamLogger(level: LogLevel): U8 {
match level {
case LogLevel.Debug then { return 0 }
case LogLevel.Info then { return 1 }
case LogLevel.Warn then { return 2 }
case _ then { return 3 }
}
}local function label for StreamLogger(level: LogLevel): String {
return ["DEBUG", "INFO", "WARN", "ERROR"][Self.severity(level)] ?? "ERROR"
}#logger-for-streamlogger/// "WARN disk almost full free=12 path=/tmp" - fields sorted by name, so a line reads the same every
/// run. Logging can't fail, so an error writing the line is dropped
method log(self: StreamLogger, level: LogLevel, message: String, fields: Map<String, Unknown>): Undefined {
if Self.severity(level) < Self.severity(self.minimum) then { return undefined }
let parts := [Self.label(level), message]
for name in fields.keys().toArray().sorted() {
parts.push(name ++ "=" ++ (fields.get(name)?.toString() ?? ""))
}
self.stream.writeText(" ".join(parts) ++ "\n") !? undefined
}#abortingpanichandlerglobal function new for AbortingPanicHandler(): AbortingPanicHandler {
return Self{}
}#panichandler-for-abortingpanichandler uses #runtime.abort
method handle(self: AbortingPanicHandler, error: Error): Never {
return #runtime.abort(error)
}#trackingallocatorLayout
global function new for TrackingAllocator(limit: U64? := undefined, inner: Allocator := &heapAllocator): TrackingAllocator {
return Self{inner := inner, limit := limit, liveBytes := 0, peakBytes := 0, count := 0}
}global method live for TrackingAllocator(self: TrackingAllocator): U64 {
return self.liveBytes
}global method peak for TrackingAllocator(self: TrackingAllocator): U64 {
return self.peakBytes
}global method allocations for TrackingAllocator(self: TrackingAllocator): U64 {
return self.count
}/// refuses a request that would take live bytes from before to after past the limit - checked before
/// inner is asked, so a refused request never reaches it
local method check for TrackingAllocator(self: TrackingAllocator, after: U64): OutOfMemoryError ! Undefined {
let limit := self.limit ?? U64.max()
if after > limit then {
error OutOfMemoryError.new("over the limit of " ++ limit.toString() ++ " bytes: " ++ after.toString() ++ " would be live")
}
}local method record for TrackingAllocator(self: TrackingAllocator, after: U64): Undefined {
set self.liveBytes := after
set self.peakBytes := self.peakBytes.max(after)
}#allocator-for-trackingallocatormethod allocate(self: TrackingAllocator, size: U64): OutOfMemoryError ! Bytes {
let after := self.liveBytes.saturatingAdd(size)
self.check(after)!!
let block := self.inner.allocate(size)!!
self.record(after)
set self.count += 1
return block
}method resize(self: TrackingAllocator, block: Bytes, size: U64): OutOfMemoryError ! Bytes {
let after := (self.liveBytes - block.size()).saturatingAdd(size)
self.check(after)!!
let resized := self.inner.resize(block, size)!!
self.record(after)
return resized
}method free(self: TrackingAllocator, block: Bytes): Undefined {
self.record(self.liveBytes - block.size())
self.inner.free(block)
}#timerLayout
uses #runtime.schedule
/// a cancelled timer's task is still on the event loop until it's due - it does nothing then
global function after for Timer(delay: Duration, task: Lambda<(), Undefined>): Timer {
let timer := Self{active := true, interval := undefined, frames := false}
#runtime.schedule(&clock.now() + delay, λ {
if timer.active then {
set timer.active := false
task()
}
})
return timer
}global function every for Timer(interval: Duration, task: Lambda<(), Undefined>): Timer {
let timer := Self{active := true, interval, frames := false}
timer.repeat(&clock.now() + interval, task)
return timer
} uses #runtime.schedule
/// the next run scheduled before this one runs - so task can cancel it - each an interval after the last
/// was due, so they don't drift
local function repeat for Timer(self: Timer, at: Instant, task: Lambda<(), Undefined>): Undefined {
#runtime.schedule(at, λ {
if self.active then {
self.repeat(at + self.interval!!, task)
task()
}
})
}global function eachFrame for Timer(task: Lambda<F64, Undefined>): Timer {
let timer := Self{active := true, interval := undefined, frames := true}
timer.frame(task)
return timer
} uses #runtime.nextFrame
local function frame for Timer(self: Timer, task: Lambda<F64, Undefined>): Undefined {
#runtime.nextFrame(λ(time) {
if self.active then {
self.frame(task)
task(time)
}
})
}global method cancel for Timer(self: Timer): Undefined {
set self.active := false
}global method isActive for Timer(self: Timer): Boolean {
return self.active
}#tostring-for-timer"Timer every 1s", "Timer each frame", "Timer once, stopped"
method toString(self: Timer): String {
let state := self.active ? "" : ", stopped"
if self.frames then { return "Timer each frame" ++ state }
if self.interval != undefined then { return "Timer every " ++ self.interval.toString() ++ state }
return "Timer once" ++ state
}#channel-type uses #runtime.shareAcrossThreads
global function new for Channel<Type>(): Channel<Type> {
return #runtime.shareAcrossThreads(Channel{waiting := Queue<Type>.new(), closed := false})
} uses #runtime.locked, #runtime.copy
global method send for Channel<Type>(self: Self, value: Type): Boolean {
return #runtime.locked(self, λ {
if self.closed then {
return false
}
self.waiting.enqueue(#runtime.copy(value))
return true
})
} uses #builtin.forever, #runtime.locked, #runtime.waitForChange
global method receive for Channel<Type>(self: Self): Type? {
for _ in #builtin.forever() {
let next := self.tryReceive()
if next != undefined or #runtime.locked(self, λ self.closed) then {
return next
}
#runtime.waitForChange(self, -1.0)
}
} uses #runtime.locked
global method tryReceive for Channel<Type>(self: Self): Type? {
return #runtime.locked(self, λ self.waiting.dequeue())
} uses #runtime.locked
global method close for Channel<Type>(self: Self): Undefined {
#runtime.locked(self, λ set self.closed := true)
} uses #runtime.locked
global method size for Channel<Type>(self: Self): U64 {
return #runtime.locked(self, λ self.waiting.size())
} uses #builtin.forever, #runtime.locked, #runtime.waitForChange
/// each tried in turn, then a short wait on the first still open - until one has a value, or all are
/// closed and empty
global function receiveAny for Channel<Type>(channels: Array<Channel<Type>>): (index: U64, value: Type)? {
for _ in #builtin.forever() {
for (index, channel) in channels.enumerate() {
let next := channel.tryReceive()
if next != undefined then {
return (index, next)
}
}
let open := channels.find(λ(channel) not #runtime.locked(channel, λ channel.closed))
if open == undefined then {
return undefined
}
#runtime.waitForChange(open, 2.0)
}
}#tostring-for-channel-typemethod toString(self: Channel<Type>): String {
return #format"Channel with #{ self.size() } waiting"
}#semaphoreLayout
global function new for Semaphore(permits: U64): Semaphore {
let channel := Channel<Boolean>.new()
for _ in 0..<permits {
channel.send(true)
}
return Semaphore{permits := channel, total := permits}
}/// taking a permit is receiving it - so waiting for one is Channel.receive's
global method acquire for Semaphore(self: Semaphore): CancelledError ! Undefined {
if self.permits.receive() == undefined then {
error CancelledError.new("stopped waiting for a permit")
}
}global method tryAcquire for Semaphore(self: Semaphore): Boolean {
return self.permits.tryReceive() != undefined
}global method release for Semaphore(self: Semaphore): Undefined {
self.permits.send(true)
}global method withPermit for Semaphore<Result>(self: Semaphore, f: Lambda<(), Result>): CancelledError ! Result {
self.acquire()!!
let result := f()
self.release()
return result
}global method available for Semaphore(self: Semaphore): U64 {
return self.permits.size()
}#tostring-for-semaphoremethod toString(self: Semaphore): String {
return #format"Semaphore with #{ self.available() } of #{ self.total } free"
}#mutexLayout
global function new for Mutex(): Mutex {
return Mutex{permit := Semaphore.new(1)}
}global method lock for Mutex(self: Mutex): CancelledError ! Undefined {
self.permit.acquire()!!
}global method tryLock for Mutex(self: Mutex): Boolean {
return self.permit.tryAcquire()
}global method unlock for Mutex(self: Mutex): Undefined {
if self.isLocked() then {
self.permit.release()
}
}global method isLocked for Mutex(self: Mutex): Boolean {
return self.permit.available() == 0
}global method locked for Mutex<Result>(self: Mutex, f: Lambda<(), Result>): CancelledError ! Result {
return self.permit.withPermit(f)!!
}#tostring-for-mutexmethod toString(self: Mutex): String {
return self.isLocked() ? "Mutex locked" : "Mutex unlocked"
}#cancellationLayout
global function new for Cancellation(): Cancellation {
return Cancellation{reasonText := undefined, deadline := undefined, parent := undefined, fixed := false, children := [], listeners := []}
}global function never for Cancellation(): Cancellation {
return Cancellation{reasonText := undefined, deadline := undefined, parent := undefined, fixed := true, children := [], listeners := []}
}global function after for Cancellation(duration: Duration): Cancellation {
return Cancellation{reasonText := undefined, deadline := &clock.now() + duration, parent := undefined, fixed := false, children := [], listeners := []}
}global method child for Cancellation(self: Cancellation): Cancellation {
let made := Cancellation{reasonText := undefined, deadline := undefined, parent := self, fixed := false, children := [], listeners := []}
if not self.fixed then {
self.children.push(made)
}
return made
}/// the first reason given is kept - then each listener runs, once, and each child is cancelled
global method cancel for Cancellation(self: Cancellation, reason: String := "cancelled"): Undefined {
if self.fixed or self.reasonText != undefined then {
return undefined
}
set self.reasonText := reason
let waiting := self.listeners
set self.listeners := []
waiting.forEach(λ(listener) listener(reason))
self.children.forEach(λ(child) child.cancel(reason))
}global method onCancel for Cancellation(self: Cancellation, f: Lambda<String, Undefined>): Undefined {
if self.fixed then {
return undefined
}
if self.reasonText != undefined then {
f(self.reasonText)
} else {
self.listeners.push(f)
}
}/// its own reason, then its deadline's, then its parent's
global method reason for Cancellation(self: Cancellation): String? {
if self.reasonText != undefined then {
return self.reasonText
}
if self.deadline != undefined and not &clock.now().isBefore(self.deadline) then {
return "its deadline, " ++ self.deadline.toString() ++ ", has passed"
}
return self.parent?.reason()
}global method isCancelled for Cancellation(self: Cancellation): Boolean {
return self.reason() != undefined
}global method check for Cancellation(self: Cancellation): CancelledError ! Undefined {
let reason := self.reason()
if reason != undefined then {
error CancelledError.new(reason)
}
}#tostring-for-cancellationmethod toString(self: Cancellation): String {
let reason := self.reason()
return reason == undefined ? "Cancellation running" : "Cancellation cancelled: " ++ reason
}#random-for-choicerandommethod between(self: ChoiceRandom, range: Range<Integer>): Integer {
return self.draw(range)
}method nextU64(self: ChoiceRandom): U64 {
return self.draw(0..=S64.max()) as U64
}method nextF64(self: ChoiceRandom): F64 {
return (self.draw(0..<1_000_000_000) as F64) / 1_000_000_000.0
}#gen-typea value at a size, from 0 (the smallest values) up to 100
Layout
local const largest: U64 := 100/// limit's share at size - none at size 0, all of it at the largest
local function scaled for Gen<Type>(limit: S64, size: U64): S64 {
return ((limit as F64) * (size.min(largest) as F64) / (largest as F64)).ceil() as S64
}global function from for Gen<Type>(make: Lambda<U64, Type>): Gen<Type> {
return Gen{make}
}global function constant for Gen<Type>(value: Type): Gen<Type> {
return Gen{make := λ(size) value}
}global function integers for Gen<Type>(low: S64 := -1000, high: S64 := 1000): Gen<S64> {
return Gen{make := λ(size) {
if low <= 0 and high >= 0 then {
return &random.between(-Self.scaled(-low, size)..=Self.scaled(high, size))
}
return &random.between(low..=low + Self.scaled(high - low, size))
}}
}global function floats for Gen<Type>(low: F64 := -1000.0, high: F64 := 1000.0): Gen<F64> {
return Gen{make := λ(size) {
let share := (size.min(largest) as F64) / (largest as F64)
let around := low <= 0.0 and high >= 0.0
let from := around ? low * share : low
let to := around ? high * share : low + (high - low) * share
return from + (to - from) * &random.nextF64()
}}
}global function booleans for Gen<Type>(): Gen<Boolean> {
return Gen{make := λ(size) &random.nextU64() % 2 == 0}
}global function strings for Gen<Type>(alphabet: String := "abcdefghijklmnopqrstuvwxyz0123456789 ", maxLength: U64 := 20): Gen<String> {
let characters := alphabet.codepoints().collect()
return Gen{make := λ(size) {
let length := &random.between(0..=Self.scaled(maxLength as S64, size)) as U64
return Array.generate(length, λ(_) characters.sample()!!).map(λ(c) c.toString()).join("")
}}
}global function arrays for Gen<Type, Item>(items: Gen<Item>, maxLength: U64 := 20): Gen<Array<Item>> {
return Gen{make := λ(size) Array.generate(&random.between(0..=Self.scaled(maxLength as S64, size)) as U64, λ(_) items.make(size))}
} uses #runtime.abort
global function oneOf for Gen<Type>(values: Array<Type>): Gen<Type> {
if values.isEmpty() then { #runtime.abort(Error.new("Gen.oneOf needs at least one value to choose from")) }
return Gen{make := λ(size) values.sample()!!}
}global method map for Gen<Type, Result>(self: Self, f: Lambda<Type, Result>): Gen<Result> {
return Gen{make := λ(size) f(self.make(size))}
} uses #runtime.abort
global method filter for Gen<Type>(self: Self, keep: Lambda<Type, Boolean>): Self {
return Gen{make := λ(size) {
for _ in 0..<100 {
let value := self.make(size)
if keep(value) then {
return value
}
}
#runtime.abort(Error.new("Gen.filter made 100 values in a row that it didn't keep"))
}}
}global method andThen for Gen<Type, Result>(self: Self, f: Lambda<Type, Gen<Result>>): Gen<Result> {
return Gen{make := λ(size) f(self.make(size)).make(size)}
}global method sample for Gen<Type>(self: Self, size: U64 := 10): Type {
return self.make(size)
}/// make at size with &random replaying choices - past their end, or where one doesn't fit, the smallest
/// value it can - giving the value and the choices it used; recording from &random when choices is undefined
local function run for Gen<Type>(make: Lambda<U64, Type>, choices: Array<U64>?, size: U64): (Type, Array<U64>) {
let used: Array<U64> := []
let replay := choices
let drawn := λ(range: Range<Integer>) {
let span := (range.end() - range.start()) as U64
let choice := replay == undefined ? (&random.between(range) - range.start()) as U64 : (replay[used.size()] ?? 0).min(span)
used.push(choice)
return range.start() + (choice as S64)
}
inject random := ChoiceRandom{draw := drawn}
return (make(size), used)
}/// the choices made simpler while the property still fails - at smaller sizes, then with chunks of them
/// left out, each zeroed, each halved - until nothing simpler fails
local function shrink for Gen<Type>(make: Lambda<U64, Type>, property: Lambda<Type, Boolean>, choices: Array<U64>, size: U64): Type {
let best := (choices, size, Self.run(make, choices, size).0)
for _ in 0..<1000 {
let (current, at, _) := best
let simpler := [0, at / 4, at / 2].map(λ(s) (current, s)) ++ [8, 4, 2, 1].flatMap(λ(chunk) (0..<current.size()).map(λ(from) (current.slice(0..<from) ++ current.slice(from + chunk..<current.size()), at))) ++ (0..<current.size()).flatMap(λ(i) [current.with(i, 0), current.with(i, current[i]!! / 2)].map(λ(c) (c, at)))
let found := simpler.find(λ((c, s)) {
let (value, used) := Self.run(make, c, s)
return not property(value) and (s < at or used.size() < current.size() or used.sum() < current.sum())
})
if found == undefined then {
return best.2
}
let (value, used) := Self.run(make, found!!.0, found!!.1)
set best := (used, found!!.1, value)
}
return best.2
}/// values from size 0 up to the largest; at the first the property fails for, the choices that made it are
/// shrunk, and the smallest value that still fails is the one reported
global method check for Gen<Type>(self: Self, property: Lambda<Type, Boolean>, runs: U64 := 100): PropertyError ! Undefined {
for run in 0..<runs {
let size := runs <= 1 ? largest : run * largest / (runs - 1)
let (value, choices) := Self.run(self.make, undefined, size)
if not property(value) then {
let smallest := Self.shrink(self.make, property, choices, size)
error PropertyError.new("the property doesn't hold for " ++ smallest.toString() ++ " - found on run " ++ (run + 1).toString() ++ " of " ++ runs.toString())
}
}
}#environment/// "maxConnections" as "MAX_CONNECTIONS"
local function variableFor for Environment(field: String): String {
return field.replaceAll(#regex"([a-z0-9])([A-Z])", "$1_$2").toUpper()
} uses #runtime.fieldsOf, #runtime.construct
/// each field's text parsed as its default value's type
global method settings for Environment<Settings: Struct>(self: Environment, defaults: Settings, prefix: String := ""): ParseError ! Settings {
let fields := #runtime.fieldsOf(defaults)
for (name, current) in fields.entries() {
let variable := prefix ++ Environment.variableFor(name)
let text := self.get(variable)
if text != undefined then {
let parsed := text.parse<current.typeOf()>()
match parsed {
case Error(e) then { error ParseError.new(variable ++ " is \"" ++ text ++ "\", which isn't a " ++ current.typeOf().name(), e) }
case value then { fields.set(name, value) }
}
}
}
return #runtime.construct(defaults.typeOf(), fields)!! as Settings
}#snapshotstore/// the first line where kept and actual differ - for the mismatch's message
local function difference for SnapshotStore(kept: String, actual: String): String {
let keptLines := kept.split("\n")
let actualLines := actual.split("\n")
for line in 0..<keptLines.size().max(actualLines.size()) {
if keptLines[line] != actualLines[line] then {
return "line " ++ (line + 1).toString() ++ ":\n kept: " ++ (keptLines[line] ?? "(no line)") ++ "\n actual: " ++ (actualLines[line] ?? "(no line)")
}
}
return "nowhere"
}global method check for SnapshotStore(self: SnapshotStore, name: String, actual: String): SnapshotError ! Undefined {
let kept := self.read(name) !? error SnapshotError.new("the snapshot '" ++ name ++ "' couldn't be read")
if kept == actual then {
return undefined
}
if kept == undefined or self.updating() then {
self.write(name, actual) !? error SnapshotError.new("the snapshot '" ++ name ++ "' couldn't be written")
return undefined
}
error SnapshotError.new("'" ++ name ++ "' isn't its snapshot - it differs at " ++ SnapshotStore.difference(kept, actual) ++ "\nrun the tests with --update-snapshots to keep the new text instead")
}