# Kex documentation Kex is a functional language with Ruby-like syntax, immutability by default, and an Elixir-style process model. - **[Guide](/guide/0.4.0-alpha/)** — the language, from the first value to processes. - **[Standard Library](/prelude/)** — every module, type and trait that ships with Kex, generated from its source. - **[Tey](/tey/)** — the package manager and build tool. Machine-readable entry points: [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt). --- # Guide Read cover to cover, or jump in anywhere. --- # Overview Kex is a functional language with Ruby-like syntax, immutability by default, and an Elixir-style process model. ## Values Everything is a value, and values are immutable unless you say otherwise: ```kex let name = "world" IO.printLine("hello, ${name}") ``` > Immutability is the default, not a mode. `var` opts out of it for one > binding. ## What you get - **UFCS** — `xs.map(f)` and `map(xs, f)` are the same call - **Traits** — behaviour shared across types - with defaults - and required methods - **Processes** — isolated, message-passing, supervised | Feature | Status | | --- | --- | | Type checker | on by default | | BEAM codegen | default backend | See the [reference](/prelude/) for the standard library. --- # Syntax A one-line block uses braces; a multi-line block uses `do ... end`. ```kex let doubled = xs.map { |x| x * 2 } ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: algebra.kex title: Algebra entities: - { kind: trait, name: "Monoid" } - { kind: trait, name: "Group" } - { kind: make, name: "Integer" } - { kind: make, name: "String" } - { kind: make, name: "[A]" } --- # Algebra Algebraic structures: combining values associatively. Kex traits do not inherit from one another, so concrete types explicitly implement every structure whose laws they satisfy. `Monoid` is "these two values combine, and there is a neutral one". Numbers, strings and lists all satisfy it, which is what lets `repeat` be written once: ```kex "ab".repeat(3) # => "ababab" [1].repeat(2) # => [1, 1] 5.repeat(3) # => 15 ``` Ordering and comparison (`Ordering`, `Comparable`) live in `comparable.kex`. ## trait `Monoid` Types whose values combine associatively and have a neutral element. Implemented by `Integer` (addition), `String` and `List` (concatenation), `Map` and both `Set` flavours (union), and `Ordering` ("first decision wins" — declared beside `Ordering` in `comparable.kex`). Implemented by [`Integer`](#make-integer), [`String`](#make-string), [`[A]`](#make-list), [`Ordering`](comparable.md#make-ordering), [`Map`](map.md#make-map), [`Queue`](data/queue.md#make-queue), [`Set`](data/set.md#make-set), [`UnorderedSet`](data/set.md#make-unorderedset), [`Stack`](data/stack.md#make-stack). ### Required functions #### `identity` ```kex identity : This ``` The neutral element: combining it with any value gives that value back. `combine` must be associative and `identity` neutral on both sides. **Returns**: the identity **Examples** ```kex Integer.identity # => 0 String.identity # => "" ``` #### `combine` ```kex combine(other: This) -> This ``` Combines this value with `other`. Must be associative: `a.combine(b).combine(c)` and `a.combine(b.combine(c))` have to agree. **Parameters** - `other` — the value to combine with **Returns**: the combined value **Examples** ```kex 5.combine(3) # => 8 "ab".combine("cd") # => "abcd" [1].combine([2]) # => [1, 2] ``` _Folding a list of values into one_ ```kex parts.reduce(String.identity) { |acc, s| acc.combine(s) } ``` ### Provided functions #### `repeat` ```kex repeat(n: Integer) -> This ``` Combines this value with itself `n` times. Repeating zero times gives the identity: `""` for a string, `0` for an integer, `[]` for a list. A negative count is invalid and ends the program. **Parameters** - `n` — how many copies to combine; must not be negative **Returns**: the repeated value **Examples** ```kex "ab".repeat(3) # => "ababab" [1].repeat(2) # => [1, 1] 5.repeat(3) # => 15 "x".repeat(0) # => "" ``` _Drawing a separator line_ ```kex IO.printLine("-".repeat(40)) ``` ## trait `Group` A `Monoid` in which every value has an inverse that combines with it to give the identity. Implemented by `Integer`, where the inverse is negation. Implemented by [`Integer`](#make-integer). ### Required functions #### `identity` ```kex identity : This ``` The neutral element. **Returns**: the identity #### `combine` ```kex combine : This -> This ``` Combines this value with `other`. **Returns**: the combined value #### `inverse` ```kex inverse : This ``` The value that combines with this one to give the identity. **Returns**: the inverse **Examples** ```kex 5.inverse # => -5 5.combine(5.inverse) # => 0 ``` ## extends `Integer` More functions for [`Integer`](number.md#make-integer), added by this module. Implements [`Monoid`](#trait-monoid), [`Group`](#trait-group). Implements `Monoid`, `Group` over `Integer` for addition. ### `identity` (from Monoid, Group) `0`: the neutral element for addition. **Returns**: `Integer` — zero **Examples** ```kex Integer.identity # => 0 ``` ### `combine` (from Monoid, Group) ```kex combine(other: This) -> This ``` Adds `other` to this integer. Addition is the monoid operation for `Integer`. **Parameters** - `other` — the integer to add **Returns**: the sum **Examples** ```kex 5.combine(3) # => 8 ``` ### `inverse` (from Group) ```kex inverse : This ``` The additive inverse: this integer negated. **Returns**: the negation **Examples** ```kex 5.inverse # => -5 (-5).inverse # => 5 ``` ## extends `String` More functions for [`String`](string.md#make-string), added by this module. Implements [`Monoid`](#trait-monoid). Implements `Monoid` over `String` for concatenation. ### `identity` (from Monoid) `""`: the neutral element for concatenation. **Returns**: `String` — the empty string **Examples** ```kex String.identity # => "" ``` ### `combine` (from Monoid) ```kex combine(other: This) -> This ``` Concatenates `other` onto this string. Concatenation is the monoid operation for `String`. **Parameters** - `other` — the string to append **Returns**: the concatenation **Examples** ```kex "ab".combine("cd") # => "abcd" ``` ## extends `[A]` More functions for [`List`](list.md#type-list), added by this module. Implements [`Monoid`](#trait-monoid). Implements `Monoid` over `List` for concatenation. ### `identity` (from Monoid) `[]`: the neutral element for concatenation. **Returns**: `[A]` — the empty list **Examples** ```kex [1].combine(List.identity) # => [1] ``` ### `combine` (from Monoid) ```kex combine(other: This) -> This ``` Concatenates `other` onto this list. Concatenation is the monoid operation for `List`. **Parameters** - `other` — the list to append **Returns**: the concatenation **Examples** ```kex [1].combine([2, 3]) # => [1, 2, 3] ``` _Flattening a list of lists_ ```kex groups.reduce([]) { |acc, g| acc.combine(g) } ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: atom.kex title: Atom entities: - { kind: module, name: "Atom" } - { kind: make, name: "Atom" } --- # Atom ## module `Atom` Atoms are named values such as `:ok`, `:error`, and `:reset`. Use them for labels, statuses, and message tags. Atoms with the same name are equal; an atom and a string are different values. ```kex :ok == :ok # => true :ok == :error # => false :ok.string # => "ok" ``` Write atoms with a leading colon. Node names can include `@`; use quotes for names containing spaces or punctuation: ```kex :app@localhost :"app@host.example.com" :"two words" ``` To convert text to an atom: ```kex "hello".as(Atom) # converts a string literal at compile time Atom.from("hello") # creates or finds an atom at run time "hello".to(Atom) # finds an existing atom: Just(:hello), or None ``` On the BEAM, atoms are never freed. Use `text.to(Atom)` for external input to avoid creating an unbounded number of atoms. ### `from` ```kex from(text: String) -> Atom ``` Returns the atom whose name is `text`. On the BEAM atoms are never freed, and a node holds at most about a million. Build atoms from a bounded set of names — node names, config keys — never from untrusted input; `text.to(Atom)` is the safe form for that, since it only finds atoms that already exist. **Parameters** - `text` — the atom's name **Returns**: the atom **Examples** ```kex Atom.from("ok") == :ok # => true Atom.from("app@${host}") # => :"app@myhost" ``` ## type `Atom` ### `string` ```kex string : String ``` Returns the atom's name as a string, without the leading colon. **Returns**: the name **Examples** ```kex :ok.string # => "ok" :"b@host.example.com".string # => "b@host.example.com" ``` ### `to` ```kex to(_) -> String? ``` Returns the atom's name as text, like `string`, through the universal conversion. The name, not the displayed form: `:ok` converts to `"ok"`, not `":ok"`, so `text.to(Atom)` and this are inverses. **Returns**: the name, always `Just` **Examples** ```kex :hello.to(String) # => Just("hello") "hello".to(Atom).flatMap { |a| a.to(String) } # => Just("hello") ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: binary.kex title: Binary entities: - { kind: type, name: "Binary" } - { kind: module, name: "Binary" } - { kind: make, name: "Binary" } --- # Binary ## type `Binary` An opaque, immutable sequence of bytes. A `Binary` never implicitly becomes text. `[Byte]` is the materialized list of bytes, while `Binary` is the storage form. A file, an HTTP body, or a digest is a `Binary`. On the BEAM it is a native binary, so slicing shares storage instead of copying. None of the operations here walks a character list. ```kex let data = Binary.fromHex("00686900").try data.length # => 4 data.at(1) # => Just(104) data.take(2).hex # => "0068" data.showValue # => "#Binary<4 bytes>" ``` The type is opaque on purpose: there is no field to reach through, so text operations cannot be run on bytes by accident. Conversions are always explicit in both directions, and both directions can fail. Runtime conversions through `to` always return an `Optional`: ```kex "árvíz".to(Binary) # => Just(#Binary<7 bytes>) : Binary? Binary.fromBytes([255]).to(String) # => None ``` `Showable` and `Inspectable` deliberately render the length alone. Neither ever decodes or interpolates the payload, so printing a binary cannot leak its contents or fail on bytes that are not text. Use `hex`, `base64`, or `to(String)` when you actually want to see it. Implements [`Showable`](kex.md#trait-showable), [`Inspectable`](kex.md#trait-inspectable). ### Functions #### `bytes` ```kex bytes : [Byte] ``` The bytes, as a list. The explicit, allocating conversion out of binary storage. Reach for `length`, `at`, `take`, and `drop` when you do not need the whole list. **Returns**: every byte, in order **Examples** ```kex Binary.fromHex("0068").try.bytes # => [0, 104] ``` #### `length` ```kex length : Integer ``` Number of bytes held. **Returns**: the byte count **Examples** ```kex "héllo".to(Binary).try.length # => 6 ``` #### `empty?` ```kex empty? : Bool ``` Whether it holds no bytes at all. **Returns**: `true` when the binary is empty **Examples** ```kex Binary.fromBytes([]).empty? # => true ``` #### `at` ```kex at(index: Integer) -> Byte? ``` The byte at `index`, counting from zero. Answers `None` for a negative or out-of-range index, so an index you did not check is something you handle rather than something that stops the program. **Parameters** - `index` — the position to read **Returns**: the byte, or `None` when the index is outside the binary **Examples** ```kex let data = Binary.fromBytes([104, 105]) data.at(0) # => Just(104) data.at(2) # => None data.at(-1) # => None ``` #### `get` ```kex get(index: Integer) -> Byte? get(index: Integer, default: Byte) -> Byte ``` The byte at `index`, counting from zero. The same as `at`, matching the `get` that `List` and `String` carry. **Parameters** - `index` — the position to read **Returns**: the byte, or `None` when the index is outside the binary **Examples** ```kex Binary.fromBytes([104, 105]).get(1) # => Just(105) Binary.fromBytes([104, 105]).get(9) # => None ``` #### `take` ```kex take(count: Integer) -> Binary ``` The first `count` bytes. Clamped at both ends, like the list operations: a `count` of zero or less answers the empty binary, and an oversized one answers the whole binary. **Parameters** - `count` — how many bytes to keep **Returns**: the leading bytes **Examples** ```kex let data = Binary.fromBytes([104, 105]) data.take(1).bytes # => [104] data.take(0).bytes # => [] data.take(999) == data # => true ``` #### `drop` ```kex drop(count: Integer) -> Binary ``` Everything after the first `count` bytes. Clamped at both ends: a `count` of zero or less answers the whole binary, and an oversized one answers the empty binary. `take(n)` and `drop(n)` therefore always concatenate back to the original. **Parameters** - `count` — how many bytes to skip **Returns**: the remaining bytes **Examples** ```kex let data = Binary.fromBytes([104, 105]) data.drop(1).bytes # => [105] (data.take(1) + data.drop(1)) == data # => true ``` #### `+` ```kex +(other: Binary) -> Binary ``` Joins two binaries end to end. **Parameters** - `other` — the bytes to append **Returns**: the two payloads, in order **Examples** ```kex Binary.fromBytes([104]) + Binary.fromBytes([105]) # => #Binary<2 bytes> ``` #### `hex` ```kex hex : String ``` The bytes as lowercase hexadecimal text, two digits per byte. **Returns**: the hexadecimal encoding **Examples** ```kex Binary.fromBytes([0, 104, 255]).hex # => "0068ff" ``` #### `base64` ```kex base64 : String ``` The bytes as standard base64 text (RFC 4648), with canonical padding. **Returns**: the base64 encoding **Examples** ```kex Binary.fromBytes([0, 104, 105, 255]).base64 # => "AGhp/w==" ``` #### `showValue` (from Showable) ```kex showValue : String ``` The length-only rendering: never the payload. **Returns**: `#Binary` **Examples** ```kex Binary.fromBytes([104, 105]).showValue # => "#Binary<2 bytes>" ``` #### `inspectValue` (from Inspectable) ```kex inspectValue(_: Bool) -> String ``` The length-only rendering: never the payload. The same as `showValue`. **Returns**: `#Binary` #### `to` ```kex to(_) -> String? ``` Decodes the payload as UTF-8, or answers `None` when the bytes are not valid text. This deliberately differs from `showValue`, which reveals only the byte count. Converting asks to inspect the actual payload and therefore makes the possibility of invalid text explicit. **Returns**: the decoded text, or `None` for invalid UTF-8 **Examples** _Decoding a text response while handling a binary one_ ```kex match response.body.to(String) do Just(text) => IO.printLine(text) None => IO.printLine("received ${response.body.length} bytes") end ``` ## module `Binary` ### `empty` (constant) ```kex empty : Binary ``` The binary holding no bytes. Backed by `Binary.fromBytes([])`, this is the natural starting value this library uses, just like (`Headers.empty`). **Examples** ```kex Binary.empty.length # => 0 ``` ### `fromBytes` ```kex fromBytes(values: [Byte]) -> Binary ``` Builds a binary from a list of bytes. Total: every `Byte` is in `0..255` by construction, so there is no rejection case. `Binary.fromBytes([])` is the empty binary. **Parameters** - `values` — the bytes to store **Returns**: the bytes, in binary storage **Examples** ```kex Binary.fromBytes([104, 105]) # => #Binary<2 bytes> Binary.fromBytes([]) # => #Binary<0 bytes> ``` ### `fromHex` ```kex fromHex(text: String) -> Binary? ``` Decodes lowercase hexadecimal text. Strict, so that decoding is the exact inverse of `hex`: uppercase digits, an odd length, whitespace, a `0x` prefix, and any non-hex character all answer `None` rather than being repaired. **Parameters** - `text` — the hexadecimal text **Returns**: the bytes, or `None` when the text is not canonical hex **Examples** ```kex Binary.fromHex("0068") # => Just(#Binary<2 bytes>) Binary.fromHex("0x68") # => None Binary.fromHex("AB") # => None Binary.fromHex("a") # => None ``` _Reading a compact identifier from configuration_ ```kex let key = env.get("CACHE_KEY").flatMap(~Binary.fromHex) let cacheEnabled? = key.present? ``` ### `fromBase64` ```kex fromBase64(text: String) -> Binary? ``` Decodes standard base64 text (RFC 4648). Strict, so that decoding is the exact inverse of `base64`: the URL-safe alphabet, missing or malformed padding, whitespace, and any other noncanonical encoding all answer `None`. **Parameters** - `text` — the base64 text **Returns**: the bytes, or `None` when the text is not canonical base64 **Examples** ```kex Binary.fromBase64("AGhp/w==") # => Just(#Binary<4 bytes>) Binary.fromBase64("AGhp_w==") # => None Binary.fromBase64("AGhp/w") # => None ``` _Loading binary key material from an environment variable_ ```kex let signingKey = ENV.get("SIGNING_KEY").flatMap(~Binary.fromBase64) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: bits.kex title: Bits entities: - { kind: module, name: "Bits" } --- # Bits ## module `Bits` Bitwise operations on `Integer`. ```kex Bits.and(0xff, 0x0f) # => 15 Bits.shiftLeft(1, 8) # => 256 Bits.test?(0b1010, 1) # => true ``` Useful for packing flags into one number, reading a binary format, or working with a protocol that describes its fields in bits. Integers are arbitrary precision, and a negative one behaves as if it were written in infinite-precision two's complement, so `Bits.not(0)` is `-1` and `Bits.and(-1, 255)` is `255`, with no word size to overflow. Shifts and bit indices count from bit 0 (the least significant bit). ### `and` ```kex and(a: Integer, b: Integer) -> Integer ``` Bitwise AND of `a` and `b`. **Parameters** - `a` — the first operand - `b` — the second operand **Returns**: the result **Examples** ```kex Bits.and(0b1100, 0b1010) # => 0b1000 Bits.and(0xff, 0x0f) # => 15 ``` _Masking off the low byte of a value_ ```kex Bits.and(value, 0xff) ``` _Testing whether a flag is present in a bitmask_ ```kex Bits.and(flags, READONLY) != 0 ``` ### `or` ```kex or(a: Integer, b: Integer) -> Integer ``` Bitwise OR of `a` and `b`. **Parameters** - `a` — the first operand - `b` — the second operand **Returns**: the result **Examples** ```kex Bits.or(0b1100, 0b1010) # => 0b1110 ``` _Combining flags into one value_ ```kex Bits.or(Bits.or(READ, WRITE), APPEND) ``` ### `xor` ```kex xor(a: Integer, b: Integer) -> Integer ``` Bitwise exclusive OR of `a` and `b`. **Parameters** - `a` — the first operand - `b` — the second operand **Returns**: the result **Examples** ```kex Bits.xor(0b1100, 0b1010) # => 0b0110 ``` _Toggling a set of flags_ ```kex Bits.xor(flags, VERBOSE) ``` ### `not` ```kex not(a: Integer) -> Integer ``` Bitwise complement of `a`. Every integer is signed and unbounded, so this is always +-(a + 1)+ rather than a width-dependent mask. **Parameters** - `a` — the operand **Returns**: the complement **Examples** ```kex Bits.not(0) # => -1 Bits.not(5) # => -6 ``` ### `shiftLeft` ```kex shiftLeft(n: Integer, by: Integer) -> Integer ``` Shifts `n` left by `by` bits. Raises if `by` is negative. **Parameters** - `n` — the value to shift - `by` — how many bits to shift by **Returns**: the shifted value **Examples** ```kex Bits.shiftLeft(1, 8) # => 256 Bits.shiftLeft(3, 2) # => 12 ``` _Building a flag constant for bit n_ ```kex Bits.shiftLeft(1, n) ``` ### `shiftRight` ```kex shiftRight(n: Integer, by: Integer) -> Integer ``` Shifts `n` right by `by` bits, propagating the sign: the result of shifting a negative number stays negative. Raises if `by` is negative. **Parameters** - `n` — the value to shift - `by` — how many bits to shift by **Returns**: the shifted value **Examples** ```kex Bits.shiftRight(256, 8) # => 1 Bits.shiftRight(-8, 1) # => -4 ``` ### `test?` ```kex test?(n: Integer, index: Integer) -> Bool ``` True when the bit at `index` of `n` is set. Raises if `index` is negative. **Parameters** - `n` — the value to inspect - `index` — the bit position, counting from 0 **Returns**: `true` when that bit is set **Examples** ```kex Bits.test?(0b1000, 3) # => true Bits.test?(0b1000, 0) # => false ``` ### `set` ```kex set(n: Integer, index: Integer) -> Integer ``` `n` with the bit at `index` set. Raises if `index` is negative. **Parameters** - `n` — the value to modify - `index` — the bit position, counting from 0 **Returns**: the modified value **Examples** ```kex Bits.set(0, 3) # => 8 ``` ### `clear` ```kex clear(n: Integer, index: Integer) -> Integer ``` `n` with the bit at `index` cleared. Raises if `index` is negative. **Parameters** - `n` — the value to modify - `index` — the bit position, counting from 0 **Returns**: the modified value **Examples** ```kex Bits.clear(0b1111, 0) # => 14 ``` ### `toggle` ```kex toggle(n: Integer, index: Integer) -> Integer ``` `n` with the bit at `index` flipped. Raises if `index` is negative. **Parameters** - `n` — the value to modify - `index` — the bit position, counting from 0 **Returns**: the modified value **Examples** ```kex Bits.toggle(0b1010, 0) # => 11 ``` ### `count` ```kex count(n: Integer) -> Integer ``` Number of set bits in `n` (population count). A negative value has infinitely many under two's complement, so this raises for one. **Parameters** - `n` — the value to measure **Returns**: the count **Examples** ```kex Bits.count(0b1011) # => 3 Bits.count(255) # => 8 ``` _How many flags are set_ ```kex Bits.count(flags) ``` ### `width` ```kex width(n: Integer) -> Integer ``` Number of bits needed to represent `n`, i.e. the position of its highest set bit plus one. Zero needs none. Raises for a negative value. **Parameters** - `n` — the value to measure **Returns**: the count **Examples** ```kex Bits.width(0) # => 0 Bits.width(255) # => 8 Bits.width(256) # => 9 ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: blankable.kex title: Blankable entities: - { kind: trait, name: "Blankable" } - { kind: make, name: "Bool" } - { kind: make, name: "Integer" } - { kind: make, name: "Float" } - { kind: make, name: "String" } - { kind: make, name: "Optional" } - { kind: make, name: "[X]" } --- # Blankable ## trait `Blankable` `Blankable`: types that can be asked whether they hold anything meaningful. A value is blank when it has no meaningful content: `None`, an empty or all-whitespace string, an empty collection, or `false`. The semantics are Rails's, and the point is the same: one question that works across types, so a validation does not need a different test per field. ```kex "".blank? # => true " ".blank? # => true (unlike " ".empty?) [].blank? # => true None.blank? # => true "hi".present? # => true ``` `present?` is the negation, and is often what reads better: ```kex fields.all? { |name, value| value.present? } ``` Implemented by [`Bool`](#make-bool), [`Integer`](#make-integer), [`Float`](#make-float), [`String`](#make-string), [`Optional`](#make-optional), [`[X]`](#make-list), [`Map`](map.md#make-map), [`Queue`](data/queue.md#make-queue), [`Set`](data/set.md#make-set), [`UnorderedSet`](data/set.md#make-unorderedset), [`Stack`](data/stack.md#make-stack). ### Required functions #### `blank?` ```kex blank? : Bool ``` Returns `true` when the value holds nothing meaningful. What that means is up to each type: whitespace-only for a string, no elements for a collection, `None` for an optional, `false` for a boolean. **Returns**: `true` when the value is blank **Examples** ```kex "".blank? # => true " ".blank? # => true "hi".blank? # => false ``` ## extends `Bool` More functions for [`Bool`](truthyable.md#make-bool), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` for `false`: the only blank boolean. **Returns**: `true` when the value is `false` **Examples** ```kex false.blank? # => true true.blank? # => false ``` ## extends `Integer` More functions for [`Integer`](number.md#make-integer), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Always `false`: every integer is a value, including zero. **Returns**: always `false` **Examples** ```kex 0.blank? # => false 42.blank? # => false ``` ## extends `Float` More functions for [`Float`](number.md#make-float), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Always `false`: every float is a value, including zero. **Returns**: always `false` **Examples** ```kex 0.0.blank? # => false ``` ## extends `String` More functions for [`String`](string.md#make-string), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the string is empty or contains only whitespace. This is what separates it from `empty?`: a string of spaces is not empty, but it is blank, and for user input that is usually the question being asked. **Returns**: `true` for an empty or whitespace-only string **Examples** ```kex "".blank? # => true " ".blank? # => true "\n\t".blank? # => true " hi ".blank? # => false ``` _Rejecting an empty answer_ ```kex let name = IO.getLine.or("") if name.blank? IO.printError("a name is required") end ``` ## extends `Optional` More functions for [`Optional`](optional.md#type-optional), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` for `None` and `false` for a `Just`: whatever it wraps. Note that `Just("")` is present, not blank: the optional holds a value, even though that value is itself blank. **Returns**: `true` for `None` **Examples** ```kex None.blank? # => true Just(1).blank? # => false Just("").blank? # => false ``` ## extends `[X]` More functions for [`List`](list.md#type-list), added by this module. Implements [`Blankable`](#trait-blankable). ### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the list has no elements. **Returns**: `true` for the empty list **Examples** ```kex [].blank? # => true [1, 2].blank? # => false ``` _Reporting an empty result set_ ```kex if matches.blank? IO.printLine("no matches") end ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: comparable.kex title: Comparable entities: - { kind: type, name: "Ordering" } - { kind: trait, name: "Comparable" } - { kind: make, name: "Number" } - { kind: make, name: "String" } - { kind: make, name: "Ordering" } --- # Comparable Ordering and comparison: what a comparison answers, and the types that have a total order. Kex traits do not inherit from one another, so concrete types explicitly implement every structure whose laws they satisfy. The two things you meet in everyday code here. `Ordering` is what a comparison answers, and it composes. This is how a multi-key sort is written without nested +if+s: ```kex a.age.compare(b.age).thenBy { a.score.compare(b.score) } ``` `Ordering` is also a `Monoid` under "first decision wins", which is what makes comparisons chain with `combine`; that conformance is declared here, beside the type it belongs to, rather than in `algebra.kex` with the `Monoid` trait. ## type `Ordering` The result of a comparison: `Less`, `Equal` or `Greater`. Declared here rather than only inside the interpreter so that `Ordering`, `Less`, `Equal` and `Greater` reach the semantic layer the same way every other stdlib type does (through the collected interfaces) instead of existing solely as native environment bindings the type checker and name resolver cannot see. **Variants** - `Less` - `Equal` - `Greater` Implements [`Monoid`](algebra.md#trait-monoid). ### Functions `Ordering` is a Monoid under "first decision wins", with Equal as identity. That is what makes multi-key comparison compose instead of nesting ifs: ```kex a.name.compare(b.name).combine(a.age.compare(b.age)) ``` `combine` evaluates its argument eagerly, so the later comparison runs even when the earlier one already decided. Use `thenBy` when that matters. #### `identity` (from Monoid) `Equal` is the neutral element, since an undecided comparison lets the next one decide. **Returns**: `Ordering` — `Equal` #### `combine` (from Monoid) ```kex combine(other: Ordering, other: Ordering) -> Ordering ``` Returns the first decisive ordering: this one if it is not `Equal`, otherwise `other`. This is what makes multi-key comparison compose. Note that `other` is evaluated eagerly, so the later comparison runs even when the earlier one has already decided: use `thenBy` when that matters. **Parameters** - `other` — the tie-breaking ordering **Returns**: the first decisive ordering **Examples** ```kex Equal.combine(Less) # => Less Less.combine(Greater) # => Less ``` _Sorting by surname, then by first name_ ```kex a.last.compare(b.last).combine(a.first.compare(b.first)) ``` #### `reverse` ```kex reverse : Ordering ``` Returns the opposite ordering: `Less` becomes `Greater`, `Greater` becomes `Less`, and `Equal` stays `Equal`. The one-word way to turn an ascending comparison into a descending one. **Returns**: the reversed ordering **Examples** ```kex Less.reverse # => Greater Greater.reverse # => Less Equal.reverse # => Equal ``` _Sorting newest first_ ```kex a.created.compare(b.created).reverse ``` #### `thenBy` ```kex thenBy(tieBreaker: Block) -> Ordering ``` Returns this ordering if it is decisive, otherwise the result of calling `tieBreaker`. The short-circuiting form of `combine`: the block runs only when this comparison is `Equal`, so a tie-breaker costs nothing once the order is already decided. Prefer it whenever the tie-breaker is more than a field read. **Parameters** - `tieBreaker` — evaluated only on a tie **Returns**: the first decisive ordering **Examples** ```kex Equal.thenBy { 2.compare(1) } # => Greater Less.thenBy { 2.compare(1) } # => Less ``` _Sorting by age, then by an expensive score_ ```kex a.age.compare(b.age).thenBy { score(a).compare(score(b)) } ``` ### From [`Monoid`](algebra.md#trait-monoid) - [`repeat`](algebra.md#monoid-repeat) — Combines this value with itself `n` times. ## trait `Comparable` Types that have a total order. Implemented by `Number`, which covers both `Integer` and `Float`, and by `String`. Implemented by [`Number`](#make-number), [`String`](#make-string). ### Required functions #### `compare` ```kex compare(other: This) -> Ordering ``` Compares this value with `other` and answers `Less`, `Equal` or `Greater`. `==` stays independent of this: a type may be equatable without being ordered. A total order: `compare` answers Less, Equal or Greater. `==` stays independent: a type may be Equatable without being ordered. **Parameters** - `other` — the value to compare against **Returns**: how this value orders against `other` **Examples** ```kex 1.compare(2) # => Less 2.compare(2) # => Equal 3.compare(2) # => Greater ``` _Sorting with an explicit comparison_ ```kex people.sort { |a, b| a.age.compare(b.age) == Less } ``` ### Provided functions #### `max` ```kex max(other: This) -> This ``` What every ordered type gets from `compare` alone. Returns the larger of this value and `other`; this value when they are equal. UFCS lets you put the first argument before the dot: `max(3, 7)` is `3.max(7)`. **Parameters** - `other` — the value to compare against **Returns**: the larger of the two **Examples** ```kex max(3, 7) # => 7 1.5.max(2.5) # => 2.5 "pear".max("apple") # => "pear" ``` #### `min` ```kex min(other: This) -> This ``` Returns the smaller of this value and `other`; this value when they are equal. **Parameters** - `other` — the value to compare against **Returns**: the smaller of the two **Examples** ```kex min(3, 7) # => 3 "pear".min("apple") # => "apple" ``` #### `clamp` ```kex clamp(low: This, high: This) -> This ``` Returns this value limited to the range `low` to `high`: `low` when it is below, `high` when it is above, and itself otherwise. **Parameters** - `low` — the smallest value allowed - `high` — the largest value allowed **Returns**: the value, clamped into `low` to `high` **Examples** ```kex 15.clamp(0, 10) # => 10 (-3).clamp(0, 10) # => 0 0.5.clamp(0.0, 1.0) # => 0.5 ``` #### `<` ```kex <(other: This) -> Bool ``` The ordering operators, from `compare` alone: whatever is `Comparable` can be written with `<` and `>`, a record as much as a number. The compiler knows them from here, not from a list of its own; numbers and strings still compare natively. **Parameters** - `other` — the value to compare against **Returns**: whether this value orders before `other` **Examples** ```kex Version { major: 1 } < Version { major: 2 } # => true ``` #### `>` ```kex >(other: This) -> Bool ``` **Parameters** - `other` — the value to compare against **Returns**: whether this value orders after `other` #### `<=` ```kex <=(other: This) -> Bool ``` **Parameters** - `other` — the value to compare against **Returns**: whether this value orders before `other` or with it #### `>=` ```kex >=(other: This) -> Bool ``` **Parameters** - `other` — the value to compare against **Returns**: whether this value orders after `other` or with it ## extends `Number` More functions for [`Number`](number.md#make-number), added by this module. Implements [`Comparable`](#trait-comparable). Number carries the implementation, so Integer and Float both inherit it rather than repeating the same three comparisons. Mixed receivers work because `<` and `>` promote across the two (`1.compare(1.0)` is Equal). ### `compare` (from Comparable) ```kex compare(other: Number) -> Ordering ``` Compares two numbers, across the `Integer`/`Float` boundary. Mixed receivers work because `<` and `>` promote across the two, so `1.compare(1.0)` is `Equal`. **Parameters** - `other` — the number to compare against **Returns**: how this number orders against `other` **Examples** ```kex 1.compare(2) # => Less 1.compare(1.0) # => Equal 2.5.compare(2) # => Greater ``` ## extends `String` More functions for [`String`](string.md#make-string), added by this module. Implements [`Comparable`](#trait-comparable). Strings order lexicographically, character by character, by code point — the same order `<` and `>` already give them. ### `compare` (from Comparable) ```kex compare(other: String) -> Ordering ``` Compares this string with `other` lexicographically, the same order `<` and `>` give strings. **Parameters** - `other` — the string to compare against **Returns**: how this string orders against `other` **Examples** ```kex "apple".compare("banana") # => Less "kex".compare("kex") # => Equal "cherry".compare("apple") # => Greater ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: console.kex title: Console entities: - { kind: module, name: "Console" } --- # Console ## module `Console` ANSI terminal styling: colors, text attributes, and cursor control. Every constant here becomes an empty string when Kex is started with `--no-colors`, or when output is not going to a terminal. You can splice them into a string unconditionally. `colorize` is usually what you want, since it applies the reset for you. **Examples** ```kex IO.printLine("${Console.GREEN}ok${Console.RESET}") IO.printLine(Console.colorize("failed", Console.RED)) ``` ### `RESET` (constant) Clears every active style. Ends a styled run started with a color or attribute constant; `colorize` appends it for you. **Examples** ```kex "${Console.BOLD}important${Console.RESET}" ``` ### `BOLD` (constant) Renders following text in bold. **Examples** ```kex IO.printLine("${Console.BOLD}Summary${Console.RESET}") ``` ### `DIM` (constant) Renders following text dimmed. Useful for secondary detail that should not compete with the main output. **Examples** ```kex IO.printLine("done ${Console.DIM}(0.4s)${Console.RESET}") ``` ### `ITALIC` (constant) Renders following text in italics, where the terminal supports it. **Examples** _Setting apart a book or command name_ ```kex IO.printLine("run ${Console.colorize("kex check", Console.ITALIC)} next") ``` ### `UNDERLINE` (constant) Underlines following text. **Examples** ```kex Console.colorize("https://kex.run", Console.UNDERLINE) ``` ### `BLINK` (constant) Makes following text blink, where the terminal supports it. Most do not, and most users would rather they did not. **Examples** _A deliberately attention-grabbing emergency indicator_ ```kex IO.printError(Console.colorize("DATABASE OFFLINE", Console.BLINK)) ``` ### `REVERSE` (constant) Swaps the foreground and background colors of following text. **Examples** _Highlighting a selected row_ ```kex Console.colorize(row, Console.REVERSE) ``` ### `HIDDEN` (constant) Hides following text: it occupies space but is not drawn. **Examples** _Reserving space so changing status text does not shift a table_ ```kex let padding = Console.colorize("..........", Console.HIDDEN) ``` ### `STRIKETHROUGH` (constant) Strikes through following text. **Examples** _Showing a completed item_ ```kex Console.colorize(task, Console.STRIKETHROUGH) ``` ### `RED` (constant) Red. Conventionally: errors and failures. **Examples** ```kex IO.printError(Console.colorize("error: ${message}", Console.RED)) ``` ### `GREEN` (constant) Green. Conventionally: success. **Examples** ```kex IO.printLine(Console.colorize("all tests passed", Console.GREEN)) ``` ### `YELLOW` (constant) Yellow. Conventionally: warnings. **Examples** ```kex IO.warn(Console.colorize("deprecated option", Console.YELLOW)) ``` ### `BLUE` (constant) Blue. Useful for informational labels and links. **Examples** ```kex IO.printLine(Console.colorize("info:", Console.BLUE) + " cache refreshed") ``` ### `MAGENTA` (constant) Magenta. Useful for highlighting a category distinct from status colors. **Examples** ```kex IO.printLine(Console.colorize("migration", Console.MAGENTA) + " 20260830") ``` ### `CYAN` (constant) Cyan. Useful for identifiers, paths, and other values inside prose. **Examples** ```kex IO.printLine("wrote ${Console.colorize(path, Console.CYAN)}") ``` ### `WHITE` (constant) White. Useful for primary text on a dark terminal. **Examples** ```kex IO.printLine(Console.colorize("Build summary", Console.WHITE)) ``` ### `GRAY` (constant) Gray. Conventionally: de-emphasised detail, like `DIM` but as a color. **Examples** ```kex IO.printLine(Console.colorize(timestamp, Console.GRAY)) ``` ### `PURPLE` (constant) Purple. An alternative accent when magenta is already in use. **Examples** ```kex IO.printLine(Console.colorize("plugin", Console.PURPLE) + ": formatter") ``` ### `CLEAR` (constant) Erases the screen and moves the cursor to the top-left corner. Use it to start a fresh frame. When you are redrawing repeatedly, `HOME` gives a smoother result: see below. **Examples** ```kex IO.print(Console.CLEAR) ``` ### `HOME` (constant) Moves the cursor to the top-left corner without erasing anything. A redraw that starts here paints over the previous frame, so there is no blank flash between frames the way `CLEAR` produces. **Examples** _An animation loop that does not flicker_ ```kex frames.each do |frame| IO.print(Console.HOME) IO.print(frame) end ``` ### `CLEARLINE` (constant) Erases the current line and returns the cursor to its start. This is how to write a progress line in place rather than one line per update. **Examples** _A progress counter on one line_ ```kex (1..100).items.each do |n| IO.print("${Console.CLEARLINE}processing ${n}/100") end IO.printLine("") ``` ### `colorize` ```kex colorize(text: String, color: String) -> String ``` Wraps `text` in `color` and appends a reset, so the style ends where the text does. Preferred over splicing the constants by hand: it cannot leak a style into the rest of the line, and it still produces plain text when color is off. **Parameters** - `text` — the text to style - `color` — a style constant from this module **Returns**: the styled text **Examples** ```kex Console.colorize("ok", Console.GREEN) Console.colorize("error", Console.RED) ``` _Styling a status word inside a longer line_ ```kex IO.printLine("build ${Console.colorize("passed", Console.GREEN)} in 2.1s") ``` ### `enabled?` (constant) ```kex enabled? : Bool ``` Returns `true` when terminal styling is on for this process. It is `false` under `--no-colors` and when output is redirected. You rarely need to check it (the constants already collapse to `""`) but it is the right test when the alternative is a different layout rather than a different color. **Examples** _Falling back to plain markers_ ```kex let bullet = Console.enabled? then "•" else "-" ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: digest.kex title: Digest entities: - { kind: module, name: "Digest" } --- # Digest ## module `Digest` Cryptographic content digests. Digests are returned as lowercase hex strings, so a Kex program never has to handle backend-specific binary values: they compare with `==`, print directly, and go into a map key unchanged. ```kex Digest.sha256("hello") # => "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ``` SHA-256 is a content fingerprint: use it to tell whether two things are the same, to key a cache, or to check that a download arrived intact. It is not a password hash: a purpose-built password KDF is what that needs. ### `sha256` ```kex sha256(content: String) -> String sha256(content: Binary) -> Binary ``` Returns the SHA-256 digest of `content`, as a 64-character lowercase hex string. **Parameters** - `content` — the text to hash **Returns**: the hex digest **Examples** ```kex Digest.sha256("hello") # => "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" Digest.sha256("") # => "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ``` _A cache key derived from an input_ ```kex let key = Digest.sha256(source).take(12) ``` _Detecting that content changed_ ```kex Digest.sha256(newText) != Digest.sha256(oldText) ``` ### `fileSha256` ```kex fileSha256(path: String) -> String? ``` Returns the SHA-256 digest of the file at `path`, or `None` when the file cannot be read. Reads the file for you, so a large file does not have to be pulled into a `String` first. **Parameters** - `path` — the file to hash **Returns**: the hex digest, or `None` **Examples** ```kex Digest.fileSha256("archive.tar.gz") # => Just("a3f1...") Digest.fileSha256("nowhere") # => None ``` _Verifying a download against a published checksum_ ```kex Digest.fileSha256(path).map { |actual| actual == expected }.or(false) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: dimensions.kex title: Dimensions entities: - { kind: module, name: "Dimensions" } --- # Dimensions ## module `Dimensions` Dimension algebra, implemented with ordinary Kex maps and integers. A base dimension is identified by the type of a marker value. Reuse that marker type to share a dimension across modules. Its display name and the marker's field values do not participate in dimensional arithmetic. Multiplication adds exponents, division subtracts them, and integer powers multiply them. Zero exponents are removed so cancellation is structural. ### `one` (constant) ```kex one : Dimension ``` The dimension with no remaining base factors. ### `base` ```kex base(marker: A) -> Dimension ``` Defines a base dimension using the nominal type of a marker value. **Parameters** - `marker` — a value of a dedicated marker type **Returns**: that base dimension, raised to the first power ### `fromPowers` ```kex fromPowers(powers: {Type: Integer}) -> Dimension ``` Normalizes a map of base identities and powers. **Parameters** - `powers` — the dimension's exponents **Returns**: the same dimension with zero exponents removed ## record `Dimension` A normalized map from base identities to integer exponents. Construct dimensions with `Dimensions.base` and compose them with arithmetic. If importing a map, use `Dimensions.fromPowers` to remove zero exponents. This runtime representation does not itself provide static measure typing. **Fields** - `powers` : {[Type](type.md#record-type): [Integer](number.md#make-integer)} ### Functions #### `dimensionless?` ```kex dimensionless? : Bool ``` Whether every base factor has cancelled. **Returns**: true for a dimensionless quantity #### `exponentOf` ```kex exponentOf(marker: A) -> Integer ``` The exponent of the supplied marker's dimension, or zero if absent. **Parameters** - `marker` — a value of the base marker type **Returns**: the base's exponent #### `*` ```kex *(other: Dimension) -> Dimension ``` Composes dimensions by adding their base exponents. **Parameters** - `other` — the other factor **Returns**: the normalized product #### `/` ```kex /(other: Dimension) -> Dimension ``` Composes dimensions by subtracting the denominator's exponents. **Parameters** - `other` — the denominator's dimension **Returns**: the normalized quotient #### `^` ```kex ^(exponent: Integer) -> Dimension ``` Raises a dimension to an integer power, including zero and negatives. **Parameters** - `exponent` — the power **Returns**: the normalized powered dimension --- --- package: prelude version: "0.4.0-rc.3-dev" source: enumerable.kex title: Enumerable entities: - { kind: trait, name: "Foldable" } - { kind: trait, name: "Enumerable" } --- # Enumerable ## trait `Foldable` Traversal operations that every foldable collection gets for free. A type becomes `Foldable` by implementing one function, `reduce`; the rest: `each`, `all?`, `any?`, `find`, `count`: are derived from it. `List`, `String`, `Map`, `Range` and both flavours of `Set` all implement it, so these functions read the same whatever you point them at. ```kex [1, 2, 3].all? { |n| n > 0 } # => true "hello".any?(~digit?) # => false { a: 1, b: 2 }.count { |k, v| v > 1 } # => 1 ``` Blocks are applied via Kex.Intrinsic.Fun.applyItem, which auto-splats a pair item into a two-argument block. That is what lets a `Map` traversal be written `{ |key, value| ... }` even though the fold hands over one tuple. Implemented by [`[X]`](list.md#make-list), [`Map`](map.md#make-map), [`Range`](range.md#make-range), [`String`](string.md#make-string), [`Queue`](data/queue.md#make-queue), [`Set`](data/set.md#make-set), [`UnorderedSet`](data/set.md#make-unorderedset), [`Stack`](data/stack.md#make-stack). ### Required functions #### `reduce` ```kex reduce(acc: A, f: (A -> T -> A)) -> A ``` Folds the collection from the left. The one operation a `Foldable` type must define; everything else here is written in terms of it. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each item **Returns**: the final accumulator **Examples** ```kex [1, 2, 3].reduce(0) { |sum, n| sum + n } # => 6 ``` ### Provided functions #### `each` ```kex each(f: (T -> Void)) -> Void ``` Calls `f` with each item, for its side effects. The loop of last resort, when what you want is a new collection rather than an effect, `map` or `filter` says so more clearly. **Parameters** - `f` — called once per item **Examples** ```kex ["ada", "grace"].each { |name| IO.printLine(name) } ``` _Over a map, the block takes key and value_ ```kex { host: "localhost", port: 8080 }.each do |key, value| IO.printLine("${key} = ${value}") end ``` #### `eachIndexed` ```kex eachIndexed(f: (T -> Integer -> Void)) -> Void ``` Calls `f` with each item and its 0-based position, for its side effects. The index is the LAST block parameter, so a `Map` entry can be taken either whole (`|entry, i|`) or spread (`|k, v, i|`). **Parameters** - `f` — called with each item and its index **Examples** ```kex ["a", "b"].eachIndexed { |s, i| IO.printLine("${i}: ${s}") } # prints: 0: a # 1: b ``` _Numbering the lines of a file_ ```kex text.lines.eachIndexed { |line, i| IO.printLine("${i + 1}\t${line}") } ``` #### `all?` ```kex all?(pred: (T -> Bool)) -> Bool ``` Returns `true` when every item satisfies `pred`. An empty collection answers `true`. **Parameters** - `pred` — the test applied to each item **Returns**: `true` when every item matches **Examples** ```kex [2, 4].all? { |n| n.even? } # => true [2, 5].all? { |n| n.even? } # => false [].all? { |n| n.even? } # => true ``` _Validating every field of a form_ ```kex fields.all? { |name, value| !value.blank? } ``` #### `any?` ```kex any?(pred: (T -> Bool)) -> Bool ``` Returns `true` when at least one item satisfies `pred`. An empty collection answers `false`. **Parameters** - `pred` — the test applied to each item **Returns**: `true` when any item matches **Examples** ```kex [1, 2, 3].any? { |n| n > 2 } # => true [1, 2, 3].any? { |n| n > 9 } # => false ``` _Detecting a flag among arguments_ ```kex args.any? { |a| a == "--verbose" } ``` #### `find` ```kex find(pred: (T -> Bool)) -> T? ``` Returns the first item satisfying `pred`, or `None` when nothing does. **Parameters** - `pred` — the test applied to each item **Returns**: the first match, or `None` **Examples** ```kex [1, 2, 3].find { |n| n > 1 } # => Just(2) [1, 2, 3].find { |n| n > 9 } # => None ``` _Looking a record up by one of its fields_ ```kex users.find { |u| u.email == target }.map { |u| u.name }.or("unknown") ``` #### `count` ```kex count(pred: (T -> Bool)) -> Integer ``` Returns how many items satisfy `pred`. **Parameters** - `pred` — the test applied to each item **Returns**: the number of matches **Examples** ```kex [1, 2, 3, 4].count { |n| n.even? } # => 2 ``` _How many lines are comments_ ```kex text.lines.count { |line| line.trim.startsWith?("#") } ``` ## trait `Enumerable` Collection-producing operations that every foldable collection gets for free. Like `Foldable`, a type joins by implementing `reduce` alone. The defaults here answer with a list, because the block may return anything at all; a type that can do better overrides them: `Map.filter` gives back a map, `Set.map` gives back a set, `String.filter` gives back a string. ```kex [1, 2, 3].map { |n| n * 2 } # => [2, 4, 6] "a1b2".filter(~digit?) # => "12" { a: 1, b: 2 }.filter { |k, v| v > 1 } # => { :b: 2 } ``` Implemented by [`[X]`](list.md#make-list), [`Map`](map.md#make-map), [`Range`](range.md#make-range), [`String`](string.md#make-string), [`Queue`](data/queue.md#make-queue), [`Set`](data/set.md#make-set), [`UnorderedSet`](data/set.md#make-unorderedset), [`Stack`](data/stack.md#make-stack). ### Required functions #### `reduce` ```kex reduce(acc: A, f: (A -> T -> A)) -> A ``` Folds the collection from the left. The one operation an `Enumerable` type must define. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each item **Returns**: the final accumulator **Examples** ```kex [1, 2, 3].reduce(1) { |product, n| product * n } # => 6 ``` ### Provided functions #### `map` ```kex map(f: (T -> B)) -> [B] ``` Applies `f` to each item and collects the results into a list. The single most useful function here: it describes what each item becomes, and leaves the walking of the collection implied. **Parameters** - `f` — applied to each item **Returns**: the results, in order **Examples** ```kex [1, 2, 3].map { |n| n * 2 } # => [2, 4, 6] ["a", "b"].map(~upperCase) # => ["A", "B"] ``` _Extracting one field from a list of records_ ```kex users.map { |u| u.email } ``` #### `mapIndexed` ```kex mapIndexed(f: (T -> Integer -> B)) -> [B] ``` Applies `f` to each item and its 0-based position, and collects the results into a list. The index is the LAST block parameter: see `eachIndexed`. **Parameters** - `f` — applied to each item and its index **Returns**: the results, in order **Examples** ```kex ["a", "b"].mapIndexed { |s, i| "${i}${s}" } # => ["0a", "1b"] ``` _Building a numbered list_ ```kex items.mapIndexed { |item, i| "${i + 1}. ${item}" }.join("\n") ``` #### `filter` ```kex filter(pred: (T -> Bool)) -> [T] ``` Returns the items for which `pred` answers `true`. **Parameters** - `pred` — the test applied to each item **Returns**: the matching items, in order **Examples** ```kex [1, 2, 3, 4].filter { |n| n.even? } # => [2, 4] ``` _Dropping blank lines_ ```kex text.lines.filter { |line| !line.trim.empty? } ``` #### `flatMap` ```kex flatMap(f: (T -> [B])) -> [B] ``` Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. Use it when each item expands into zero or more results: `map` would give you a list of lists. **Parameters** - `f` — applied to each item, returning a list **Returns**: all the results, concatenated **Examples** ```kex [[1, 2], [3]].flatMap { |xs| xs } # => [1, 2, 3] ["a b", "c"].flatMap { |s| s.split(" ") } # => ["a", "b", "c"] ``` _Every tag used across a list of posts_ ```kex posts.flatMap { |p| p.tags } ``` #### `collect` ```kex collect(f: (T -> B?)) -> [B] ``` Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. This is filter and map fused into one pass, which is what you want whenever the test and the transformation are the same operation. Parsing is the classic case: an item either yields a value or it does not. **Parameters** - `f` — applied to each item, returning an optional **Returns**: the present values, unwrapped, in order **Examples** ```kex ["1", "x", "3"].collect { |s| s.to(Integer) } # => [1, 3] ``` _Looking several keys up at once, skipping the missing ones_ ```kex keys.collect { |k| config.get(k) } ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: env.kex title: ENV entities: - { kind: module, name: "ENV" } --- # ENV ## module `ENV` The process environment, as an immutable `Map` snapshot taken at startup. `ENV` supports the whole `Map` API: `get`, `has?`, `keys`, `values`, `count`, `each`, `entries`, so reading a variable looks like any other map lookup: ```kex ENV.get("HOME") # => Just("/home/ada") ENV.get("LOG_LEVEL", "info") # => "info" when unset ENV.has?("PATH") # => true ``` The snapshot itself is immutable, but the global `ENV` namespace is an ambient input: the same call can answer differently between runs without anything appearing in a function's arguments. That is why reading it is `foul`. When you would rather the dependency be visible, take it as a parameter: `main` receives the same snapshot as its second argument, and reading a parameter is pure: ```kex main(args, env) do let level = env.get("LOG_LEVEL", "info") IO.printLine("log level: ${level}") end ``` ### `get` ```kex get(key: String, default) -> String? ``` Returns the value of the environment variable `key`, or `None` when it is not set. **Parameters** - `key` — the variable name **Returns**: the value, or `None` **Examples** ```kex ENV.get("HOME") # => Just("/home/ada") ENV.get("NOT_SET") # => None ``` _Handling both cases explicitly_ ```kex match ENV.get("HOME") do Just(home) => IO.printLine("home: ${home}") None => IO.printError("HOME is not set") end ``` ### `has?` ```kex has?(key: String) -> Bool ``` Returns `true` when `key` is set, whatever its value. Distinguishes an unset variable from one set to the empty string, which a `get` with a default cannot. **Parameters** - `key` — the variable name **Returns**: `true` when the variable is set **Examples** ```kex ENV.has?("PATH") # => true ENV.has?("NOT_SET") # => false ``` _Turning a flag on by its presence alone_ ```kex let debug = ENV.has?("DEBUG") ``` ### `keys` ```kex keys : [String] ``` Returns every variable name in the environment. **Returns**: the variable names **Examples** ```kex ENV.keys.count # => 47 ``` _Every variable belonging to one tool_ ```kex ENV.keys.filter { |name| name.startsWith?("KEX_") } ``` ### `values` ```kex values : [String] ``` Returns every variable value in the environment. **Returns**: the values **Examples** ```kex ENV.values.count # => 47 ``` ### `count` ```kex count : Integer ``` Returns how many variables the environment has. **Returns**: the number of variables **Examples** ```kex IO.printLine("environment has ${ENV.count} variables") ``` ### `each` ```kex each(f: (String -> String -> Void)) -> Void ``` Calls `f` with each variable's name and value. **Parameters** - `f` — called once per variable **Examples** _Dumping the environment_ ```kex ENV.each { |name, value| IO.printLine("${name}=${value}") } ``` ### `entries` ```kex entries : [(String, String)] ``` Returns the environment as a list of `(name, value)` pairs. The bridge to the `List` operations, sorting, grouping, taking a slice. **Returns**: the variables **Examples** _Printing the environment in name order_ ```kex ENV.entries.each { |name, value| IO.printLine("${name}=${value}") } ``` ### `set` ```kex set(name: String, value: String) -> Void ``` Sets an environment variable for this process and every child it starts. `ENV` is a snapshot, and the write rebuilds it: a later `ENV.get` answers what was set, not what the process started with. This is how a program decides what a child sees. `Kex.AST`, for instance, shells out to the compiler named by `$KEX`, so a tool that knows which compiler it means says so here rather than hoping `PATH` agrees. Sets a variable for THIS process and every child it starts. `ENV` is a snapshot, so it is rebuilt by the write: a later `ENV.get` answers what was set, not what the process started with. This is how a program decides what a child sees: `Kex.AST` shells out to the compiler named by `$KEX`, so a tool that knows which compiler it means says so here rather than hoping PATH agrees. **Parameters** - `name` — the variable to set - `value` — the value to give it **Examples** ```kex ENV.set("KEX", "/usr/local/bin/kex") ENV.get("KEX") # => Just("/usr/local/bin/kex") ``` _Making a child process quiet_ ```kex ENV.set("NO_COLOR", "1") ``` ### `unset` ```kex unset(name: String) -> Void ``` Removes an environment variable from this process and its children. **Parameters** - `name` — the variable to remove **Examples** ```kex ENV.unset("DEBUG") ENV.has?("DEBUG") # => false ``` _Making sure a child does not inherit a setting_ ```kex ENV.unset("KEX_TRACE") ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: errorable.kex title: Errorable entities: - { kind: trait, name: "Errorable" } - { kind: record, name: "ParseError" } --- # Errorable ## trait `Errorable` `Errorable`: the trait for values that describe a failure. Implemented by error types that carry a human-readable message, so a generic handler can display or log any error without knowing its concrete type or its structured fields. ```kex foul report(e: Errorable) -> Void do IO.printError("error: ${e.message}") end ``` ### Required functions #### `message` ```kex message : String ``` A human-readable description of what went wrong. Written for a person reading output, not for a program to match on: branch on the error's own type or fields for that. **Returns**: the failure message **Examples** ```kex IO.printError("error: ${e.message}") ``` ## record `ParseError` A parse failure, with everything needed to report it or to carry on from it. Answered by `Integer.parse`, `Float.parse` and `Number.parse` when the input is not what they expected. Reach for those over `to(Integer)` exactly when this detail matters: `to` answers a plain `None`. ```kex Integer.parse("12x") # => Error(ParseError { input: "12x", position: 2, value: 12, # message: "trailing characters after integer", # rest: "x" }) ``` The `position` and `rest` are what make a hand-written scanner possible: the error says exactly where it stopped and what was left. **Fields** - `input` : [String](string.md#make-string) - `position` : [Integer](number.md#make-integer) - `value` : Any - `message` : [String](string.md#make-string) - `rest` : [String](string.md#make-string) --- --- package: prelude version: "0.4.0-rc.3-dev" source: evaluator.kex title: Evaluator entities: - { kind: module, name: "Evaluator" } --- # Evaluator ## module `Evaluator` Running Kex source code at run time, in a sandbox. Each call builds a fresh, isolated evaluator: the caller's environment is never shared, and the evaluated code sees only what `EvaluatorOptions` allows. Step and depth limits make a runaway program stop rather than hang. ```kex Evaluator.runExpression("1 + 2") # => Ok(3) Evaluator.runExpression("[3,1,2].sort") # => Ok([1, 2, 3]) ``` This is for evaluating expressions your program was given: a formula in a config file, a filter typed by a user. Every result is a `Result`, so a syntax error or a rejected call is a value you handle. ### `run` ```kex run(source: String) -> Result run(source: String, opts: EvaluatorOptions) -> Result ``` Evaluates a whole Kex program and returns its result. The source may declare functions, records and a `main`, exactly as a file would. Anything that goes wrong: a parse error, a call the sandbox does not allow, running past the step limit: comes back as `Error` with a message. **Parameters** - `source` — the Kex program to run - `opts` — what the sandbox allows; omitted for the defaults **Returns**: the program's result, or why it failed **Examples** ```kex Evaluator.run("main do 1 + 1 end") # => Ok(2) ``` _Running a user-supplied script with a tighter budget_ ```kex Evaluator.run(script, EvaluatorOptions { allow: [:Math, :List], maxSteps: 10_000 }) ``` ### `runExpression` ```kex runExpression(source: String) -> Result runExpression(source: String, opts: EvaluatorOptions) -> Result ``` Evaluates a single Kex expression and returns its value. The form to reach for when the input is a formula rather than a program: there is no `main` to write and no declarations to skip past. **Parameters** - `source` — the Kex expression to evaluate - `opts` — what the sandbox allows; omitted for the defaults **Returns**: the value, or why it failed **Examples** ```kex Evaluator.runExpression("1 + 2") # => Ok(3) Evaluator.runExpression("[3,1,2].sort") # => Ok([1, 2, 3]) Evaluator.runExpression("nope()") # => Error("Runtime error: ... Undefined function: nope") ``` _Evaluating a formula from a config file_ ```kex Evaluator.runExpression(formula).or(0) ``` ## record `EvaluatorOptions` What an evaluated program is allowed to reach, and how long it may run. The defaults are already conservative: pure computation over the common data types, no filesystem, no network, no processes. Narrow `allow` further, or lower the limits, when the source is less trusted still. **Fields** - `allow` : [[Atom](atom.md#make-atom)] (optional) - `modules` : {[String](string.md#make-string): {[String](string.md#make-string): (Any) -> Any}} (optional) - `maxSteps` : Int (optional) - `maxDepth` : Int (optional) --- --- package: prelude version: "0.4.0-rc.3-dev" source: feed.kex title: Feed entities: - { kind: type, name: "Feed" } - { kind: module, name: "Feed" } - { kind: make, name: "Feed" } --- # Feed ## type `Feed` A one-shot sequence over a source that can only be read once. A feed is what `Stream` is not: reading it CONSUMES it. Where a stream remembers its elements so it can be walked again, a feed keeps nothing, and that is the point: it is how to walk a file, a socket or a device larger than memory, which a stream cannot do while anything still holds its start. ```kex foul lines = FS.File.feed("huge.log").or(Feed.empty) lines.each { |line| IO.printLine(line) if line.contains?("ERROR") } ``` `map`, `filter` and `drop` answer a feed over the SAME cursor rather than a second cursor over the same source, so a pipeline is one pass: ```kex FS.File.feed("app.log").or(Feed.empty) .filter { |line| line.contains?("ERROR") } .take(10) ``` Taking twice answers two different windows: the first ten, then the ten after them. That is the whole difference from `Stream`, where taking twice answers the same ten. When you want the stream behaviour on a small source, `toStream` asks for it explicitly, at the cost of holding what it reads. ### Functions Reading a feed consumes its cursor. Obtain a new feed when you need to traverse the same data again. Opening a file-backed feed is `foul`; consuming an existing feed does not require another capability. #### `pull` ```kex pull : A? ``` Answers the next element and consumes it, or `None` once the source is spent. NOT named `next`: that is the loop-continue keyword, and the lexer reads it as one even after a dot, so a call to it silently swallows the rest of the enclosing block. **Returns**: the next element, or `None` **Examples** ```kex let feed = Feed.Elements([1, 2]) feed.pull # => Just(1) feed.pull # => Just(2) feed.pull # => None ``` #### `spent?` ```kex spent? : Bool ``` Whether the source has run out. `false` does not promise another element: only that the feed has not been told otherwise yet. The source is asked, and may end, on the next read. **Returns**: `true` once the feed is spent **Examples** _Draining a feed a piece at a time_ ```kex let jobs = Feed.Elements(["resize", "index"]) jobs.spent? # => false jobs.take(2) jobs.spent? # => true ``` #### `take` ```kex take(n: Integer) -> [A] ``` Returns the next `n` elements as a list, consuming them. Answers fewer than `n` when the source ends first, and `[]` once it is spent. Unlike `Stream.take`, calling it twice walks forward. **Parameters** - `n` — how many elements to consume **Returns**: up to `n` elements **Examples** _Two windows, not the same one twice_ ```kex let feed = Feed.Elements([1, 2, 3, 4]) feed.take(2) # => [1, 2] feed.take(2) # => [3, 4] ``` #### `drop` ```kex drop(n: Integer) -> Feed ``` Discards the next `n` elements and answers the feed. There is only ever one cursor, so this hands back the same feed rather than a second one positioned differently. **Parameters** - `n` — how many elements to discard **Returns**: the same feed, advanced **Examples** ```kex FS.File.feed("log.txt").or(Feed.empty).drop(1).take(3) ``` #### `map` ```kex map(f: (A -> B)) -> Feed ``` Returns a feed with `f` applied to each element. `f` runs as elements are read, and only for those that are. The result draws from the same cursor, so reading it consumes the receiver too. **Parameters** - `f` — applied to each element **Returns**: the mapped feed **Examples** ```kex FS.File.feed("log.txt").or(Feed.empty).map(~trim).take(5) ``` #### `filter` ```kex filter(pred: (A -> Bool)) -> Feed ``` Returns a feed of only the elements `pred` accepts. Reading one element of the result may consume many of the receiver's. **Parameters** - `pred` — the test applied to each element **Returns**: the filtered feed **Examples** _The first ten errors in a long log_ ```kex FS.File.feed("app.log").or(Feed.empty) .filter { |line| line.contains?("ERROR") } .take(10) ``` #### `each` ```kex each(f: (A -> Void)) -> Void ``` Applies `f` to every remaining element, draining the feed. **Parameters** - `f` — applied to each element **Examples** ```kex FS.File.feed("log.txt").or(Feed.empty).each { |line| IO.printLine(line) } ``` #### `collect` ```kex collect : [A] ``` Drains the feed into a list. This is the operation that gives up constant space, which is why it is named rather than implicit: on a source too large to hold, use `each`. **Returns**: every remaining element **Examples** ```kex FS.File.feed("short.txt").or(Feed.empty).collect ``` _Loading a small configuration file for repeated passes_ ```kex let rules = FS.File.feed("rules.txt") .or(Feed.empty) .map(~trim) .filter(~present?) .collect ``` #### `toStream` ```kex toStream : Stream ``` Returns a `Stream` drawn from the feed. The stream remembers what it reads, so it can be walked again: at the cost of holding every element forced through it. Worth it for a source small enough to replay; the opposite of what a feed is for otherwise. **Returns**: a replayable view of the remaining elements **Examples** ```kex let lines = FS.File.feed("small.txt").or(Feed.empty).toStream lines.take(2) # => ["one", "two"] lines.take(2) # => ["one", "two"] : a stream, so the same two ``` ## module `Feed` Constructors for `Feed`. ### `empty` (constant) ```kex empty : Feed ``` A feed with nothing in it, already spent. Mostly the `or` half of a failed open: `FS.File.feed(path).or(Feed.empty)` reads nothing rather than stopping the program. **Examples** ```kex Feed.empty.take(3) # => [] ``` ### `Elements` ```kex Elements(xs: [A]) -> Feed ``` A feed over a list that is already in hand. The elements are there, so nothing is saved by feeding them: this is for standing in for a real source, in a test, with something that consumes the way the real one does. **Parameters** - `xs` — the elements to feed **Returns**: a feed over `xs` **Examples** ```kex let feed = Feed.Elements(["one", "two"]) feed.take(1) # => ["one"] feed.take(1) # => ["two"] : the first one is gone ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: filehandle.kex title: FileHandle entities: - { kind: type, name: "ReadError" } - { kind: trait, name: "Readable" } - { kind: trait, name: "Writable" } - { kind: type, name: "FileHandle" } - { kind: make, name: "FileHandle" } - { kind: make, name: "FileHandle" } - { kind: make, name: "FileHandle" } --- # FileHandle ## type `ReadError` Why a read failed. `ReadFailed` means the source refused the read. `InvalidUtf8` means bytes were read but are not valid UTF-8, and carries the byte offset of the first malformed sequence, relative to that one operation. A failed read consumes the bytes it attempted to read and never substitutes U+FFFD: use `readBytes` instead of a text read when you need the original bytes. **Variants** - `ReadFailed` - `InvalidUtf8(Integer)` ## trait `Readable` `Readable`: a source that yields text. `IO.in` and file handles opened for reading are `Readable`. Accept this trait when your function should work with either source. ```kex foul firstLine(source: Readable) -> String do source.getLine.try.or("(empty)") end firstLine(IO.in) firstLine(FS.File.open("notes.txt", Read).try) ``` Implemented by [`FileHandle`](#make-filehandle-canread). ### Required functions #### `getLine` ```kex getLine : Result ``` Reads the next line, without its newline. **Returns**: the next line, `Ok(None)` at end of input, or the failure #### `get` ```kex get : Result ``` Reads a single character, as a one-character `String`. Reads one complete Unicode scalar, not one byte. **Returns**: the next character, `Ok(None)` at end of input, or the failure #### `readLine` ```kex readLine : Result ``` Reads the next line, without its newline. The same as `getLine`. **Returns**: the next line, `Ok(None)` at end of input, or the failure #### `read` ```kex read : Result ``` Reads everything remaining, as one `String`. Draining an exhausted source answers `Ok("")`. **Returns**: the remaining contents, or the failure #### `readBytes` ```kex readBytes : Result ``` Reads everything remaining as raw bytes, without decoding it as text. The byte counterpart of `read`: it returns the remaining bytes without validating UTF-8. Bytes consumed by a failed text read are not returned by a subsequent `readBytes` call. Draining an exhausted source answers `Ok(Binary.fromBytes([]))`. **Returns**: the remaining bytes, or the failure #### `eof?` ```kex eof? : Bool ``` Returns `true` when the source has reached its end. **Returns**: `true` at end of input #### `atEnd?` ```kex atEnd? : Bool ``` Returns `true` when the source has reached its end. The same as `eof?`. **Returns**: `true` at end of input ## trait `Writable` `Writable`: a sink that accepts text. Accept a `Writable` parameter to let callers choose standard output, standard error, or a file as the destination. ```kex foul report(out: Writable, lines: [String]) -> Void do lines.each { |line| out.printLine(line) } end report(IO.out, results) report(IO.error, warnings) report(FS.File.open("report.txt", Write).try, results) ``` Write functions accept any `Showable` value and return `Void`. Implemented by [`FileHandle`](#make-filehandle-canwrite). ### Required functions #### `printLine` ```kex printLine(content: Showable) -> Void ``` Writes `content` followed by a newline. **Parameters** - `content` — the value to write #### `print` ```kex print(content: Showable) -> Void ``` Writes `content` with no trailing newline. **Parameters** - `content` — the value to write #### `writeLine` ```kex writeLine(content: Showable) -> Void ``` Writes `content` followed by a newline. The same as `printLine`. **Parameters** - `content` — the value to write #### `write` ```kex write(content: Showable) -> Void ``` Writes `content` with no trailing newline. The same as `print`. **Parameters** - `content` — the value to write #### `writeBytes` ```kex writeBytes(content: Binary) -> Void ``` Writes `content` as raw bytes, with no trailing newline. The byte counterpart of `write`: the payload goes to the sink exactly as given. It never renders the value, so a `Binary` reaches the sink as its bytes rather than as the `#Binary` that `Showable` would print. **Parameters** - `content` — the bytes to write ## type `FileHandle` An open file, obtained from `FS.File.open`. The two type parameters record what the handle is allowed to do: `R` is `CanRead` or `CannotRead`, `W` is `CanWrite` or `CannotWrite`. `FS.File.open` picks them from the mode you pass, so calling `write` on a handle opened `Read` is a compile error rather than a run-time failure. ```kex using FS main do match FS.File.open("notes.txt", Read) do Ok(handle) => do IO.printLine(handle.read.or("")) handle.close end Error(e) => IO.printError("cannot open: ${e}") end end ``` Reach for a handle when you want to walk a large file a line at a time, or make many small writes. When a file fits comfortably in memory, `FS.File.read` and `FS.File.write` are shorter and need no closing. To be rid of the closing entirely, pass `FS.File.open` a block. The handle functions are `foul`: obtaining a handle is not an effect, but reading or writing through one is, so a function that does so is `foul` no matter where the handle came from. Injection makes a thing substitutable, not pure. Implements [`Readable`](#trait-readable), [`Writable`](#trait-writable). ### Functions #### `close` ```kex close : Void ``` Closes the handle, flushing anything still buffered. Close every handle you open. A written file is not guaranteed to be complete on disk until its handle is closed. Passing `FS.File.open` a block closes the handle for you. **Examples** ```kex match FS.File.open("out.txt", Write) do Ok(handle) => do handle.printLine("hello") handle.close end Error(_) => IO.printError("cannot open out.txt") end ``` #### `seek` ```kex seek(offset: Integer) -> Result ``` Moves the handle's cursor to an absolute byte offset from the start of the file. Read and write share one cursor, so this repositions both — a `readLine` right after `seek(0)` starts over from the top, and a `write` right after does too, overwriting from that point. **Parameters** - `offset` — the byte offset to seek to, from the start of the file **Returns**: `Ok` on success, or why the seek failed **Examples** _Reading a length-prefixed record, then rewinding past it_ ```kex let length = handle.readLine.or("0").to(Integer).or(0) let record = handle.readBytes.try handle.seek(0) ``` #### `reset` ```kex reset : Result ``` Moves the handle's cursor back to the start of the file — the same as `seek(0)`, for the common case of re-reading a handle from the top. **Returns**: `Ok` on success, or why the reset failed **Examples** _Reading a file twice_ ```kex let firstPass = handle.read.or("") handle.reset let secondPass = handle.read.or("") ``` ### On `FileHandle` #### `getLine` (from Readable) ```kex getLine : Result ``` Reads the next line from the handle, without its newline. Answers `None` at end of file, which is what makes it usable as a loop condition. The same operation as `readLine`, under the name `IO.getLine` uses. **Returns**: the next line, or `None` at end of file **Examples** _Walking a file line by line_ ```kex foul echo(handle: FileHandle) -> Void do match handle.getLine do Just(line) => do IO.printLine(line) echo(handle) end None => () end end ``` #### `get` (from Readable) ```kex get : Result ``` Reads a single character from the handle, as a one-character `String`. Answers `None` at end of file. **Returns**: the next character, or `None` at end of file **Examples** ```kex let firstChar = handle.get.or("") ``` #### `readLine` (from Readable) ```kex readLine : Result ``` Reads the next line from the handle, without its newline. The same as `getLine`, named for reading from a file rather than from a console. **Returns**: the next line, or `None` at end of file **Examples** ```kex let header = handle.readLine.or("") ``` _Reading the first three lines_ ```kex let head = (1..3).items.map { |_| handle.readLine.or("") } ``` #### `read` (from Readable) ```kex read : Result ``` Reads everything remaining in the file and returns it as one `String`. Reads from the current position, so calling it after a `readLine` gives the rest of the file rather than the whole of it. **Returns**: the remaining contents **Examples** ```kex let body = handle.read.or("") ``` _Skipping a header line, then taking the rest_ ```kex handle.readLine let body = handle.read.or("") ``` #### `readBytes` (from Readable) ```kex readBytes : Result ``` Reads all remaining bytes from the handle without decoding them. Unlike `read`, this accepts arbitrary binary data and cannot fail because the input is not valid UTF-8. It starts at the handle's current position. **Returns**: the remaining bytes **Examples** _Reading a file with an unknown encoding_ ```kex let payload = handle.readBytes.try ``` #### `eof?` (from Readable) ```kex eof? : Bool ``` Returns `true` when the handle has reached the end of the file. **Returns**: `true` at end of file **Examples** ```kex handle.eof? # => false, before anything has been read ``` #### `atEnd?` (from Readable) ```kex atEnd? : Bool ``` Returns `true` when the handle has reached the end of the file. The same as `eof?`, spelled out. **Returns**: `true` at end of file **Examples** ```kex if !handle.atEnd? IO.printLine(handle.readLine.or("")) end ``` #### `feed` ```kex feed : Feed? ``` Returns the handle's remaining lines as a lazy `Feed`. Lines are read on demand off the handle's own position, so this is how to look at the start of a very large file, or process one without holding it all in memory. The feed shares the handle's cursor: interleaving `readLine` with it advances one position through one open file. The feed ends at the last line, so taking more lines than the file has answers just the lines there are. NOT part of `Readable`: a feed is neither pure nor reusable, so requiring it of every `Readable` would put a foul, one-shot operation on types that have no such cursor to offer. It stays a FileHandle function. **Returns**: the lines as a feed, or `None` **Examples** _The first ten lines of a large file_ ```kex handle.feed .map { |lines| lines.take(10) } .or([]) ``` ### On `FileHandle` #### `writeBytes` (from Writable) ```kex writeBytes(content: Binary) -> Void ``` Writes `content` verbatim, without text encoding or a trailing newline. **Parameters** - `content` — the bytes to write **Examples** _Copying an opaque payload_ ```kex destination.writeBytes(source.readBytes.try) ``` #### `printLine` (from Writable) ```kex printLine(content: Showable) -> Void ``` Writes `content` followed by a newline. **Parameters** - `content` — the value to write **Examples** _Writing a report line by line_ ```kex rows.each { |row| handle.printLine(row) } ``` #### `print` (from Writable) ```kex print(content: Showable) -> Void ``` Writes `content` with no trailing newline. **Parameters** - `content` — the value to write **Examples** _Building a line from pieces_ ```kex handle.print("name,") handle.print("age") handle.printLine("") ``` #### `writeLine` (from Writable) ```kex writeLine(content: Showable) -> Void ``` Writes `content` followed by a newline. The same as `printLine`, named for writing to a file rather than to a console. **Parameters** - `content` — the value to write **Examples** ```kex handle.writeLine("done") ``` #### `write` (from Writable) ```kex write(content: Showable) -> Void ``` Writes `content` with no trailing newline. The same as `print`. **Parameters** - `content` — the value to write **Examples** _Writing a whole document in one call_ ```kex handle.write(rendered) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: fs.kex title: FS entities: - { kind: module, name: "FS" } --- # FS ## module `FS` The filesystem: reading and writing files, walking directories, and manipulating paths. `FS` is not in the prelude: start with `using FS`. ```kex using FS main do match FS.File.read("config.txt") do Just(text) => IO.printLine(text.lines.count) None => IO.printError("config.txt is missing") end end ``` It is organised in three parts: ```kex FS.File reading, writing, copying and deleting files FS.Directory creating, listing and removing directories FS.Path pure path arithmetic, with no filesystem access at all ``` The first two are capabilities: everything in them touches the real filesystem, so they can only be called from `foul` code, and a test can replace the whole of `FS.File` for a lexical region rather than mutating global state. `FS.Path` is ordinary pure code. Reading and writing a file answer with a `Result`, and most other operations with an `Optional` or a `Bool`, so a missing file or a failed write is an ordinary value you handle rather than an exception you catch. ### `permissionMode` ```kex permissionMode(permissions: [Permission]) -> Integer ``` The POSIX mode `FS.File.changePermission` gives a file: the owner gets every listed permission, everyone else gets them without `Writable`. **Parameters** - `permissions` — everything the file should allow **Returns**: the permission bits **Examples** ```kex FS.permissionMode([FS.Readable, FS.Writable]) # => 420, which is 0o644 ``` ## type `FilePath` A filesystem path. An alias for `String`, so every `String` function applies to one; `FS.Path` adds the path-aware operations. **Variants** - `String` ## type `FileModes` How a file should be opened: `Read` to read it, `Write` to replace it, `Append` to add to its end, `ReadWrite` for both. The mode chosen decides what type the resulting `FileHandle` has, and therefore which operations the compiler will let you call on it. **Variants** - `Read` - `Write` - `Append` - `ReadWrite` ## type `ReadPermission` Whether a `FileHandle` may be read from. Part of the handle's type rather than a runtime flag, so reading from a write-only handle is a compile error. **Variants** - `CanRead` - `CannotRead` ## type `WritePermission` Whether a `FileHandle` may be written to. Part of the handle's type, so writing to a read-only handle is a compile error. **Variants** - `CanWrite` - `CannotWrite` ## type `Permission` Something a file may allow, for `FS.File.changePermission`. Named for the intent rather than for POSIX bits, so the same list means the right thing on every system. **Variants** - `Readable` - `Writable` - `Executable` ## type `FileError` A file operation that failed, carrying the path it failed on. `OpenFailed`, `ReadFailed` and `WriteFailed` mean the filesystem refused the operation. `InvalidUtf8` means the bytes were read but are not valid UTF-8, and carries the byte offset of the first malformed sequence. **Variants** - `OpenFailed(FilePath)` - `ReadFailed(FilePath)` - `WriteFailed(FilePath)` - `InvalidUtf8(FilePath, Integer)` ## type `FileKind` What a path names, as `FS.File.info` reports it: a regular `:file`, a `:directory`, a `:symlink` (the link itself, never what it points at), or `:other` for devices, sockets and pipes. ## record `FileInfo` What one `stat` of a path says about it. **Fields** - `kind` : [FileKind](#type-fs-filekind) - `size` : [Integer](number.md#make-integer) - `modified` : [DateTime](time.md#record-datetime) ## module `FS.File` Reading and writing files. A capability: every member reaches the real filesystem, so a test can replace the whole thing for a lexical region with `with FS.File = ...` to test file operations without touching the filesystem. `FS.Path` operations only manipulate path strings. ### `open` ```kex open(path: FilePath, mode: Read) -> Result, FileError> open(path: FilePath, mode: Write) -> Result, FileError> open(path: FilePath, mode: Append) -> Result, FileError> open(path: FilePath, mode: ReadWrite) -> Result, FileError> open(path: FilePath, mode: Read) -> (FileHandle -> A) -> Result open(path: FilePath, mode: Write) -> (FileHandle -> A) -> Result open(path: FilePath, mode: Append) -> (FileHandle -> A) -> Result open(path: FilePath, mode: ReadWrite) -> (FileHandle -> A) -> Result ``` Opens `path` and returns a `FileHandle` for it. The mode decides the handle's type, and the type decides what you may do with it: a handle opened `Read` has no `write`, a handle opened `Write` has no `read`. That is checked at compile time, not at run time. Use the whole-file `read` and `write` below when a file fits in memory and you have no reason to hold it open; reach for a handle when you want to stream through a large file or make many small writes. Close the handle when you are done with it. **Parameters** - `path` — the file to open - `mode` — `Read`, `Write`, `Append` or `ReadWrite` **Returns**: the handle, or `OpenFailed` **Examples** _Reading a file line by line_ ```kex match FS.File.open("log.txt", Read) do Ok(handle) => do IO.printLine(handle.readLine.or("")) handle.close end Error(e) => IO.printError("cannot open: ${e}") end ``` _Writing a report_ ```kex match FS.File.open("report.txt", Write) do Ok(handle) => do rows.each { |row| handle.printLine(row) } handle.close end Error(_) => IO.printError("cannot write report") end ``` _A failure names the path_ ```kex FS.File.open("missing/x.txt", Read) # => Error(OpenFailed("missing/x.txt")) ``` ### `read` ```kex read(path: FilePath) -> Result ``` Reads the whole file and decodes it as UTF-8 text. Answers `Error(ReadFailed(path))` when the file does not exist or cannot be read, and `Error(InvalidUtf8(path, offset))` when the bytes are not valid UTF-8, so a missing or non-text file is something you handle rather than something that stops the program. Reach for `FS.File.readBytes` when the contents are not text. **Parameters** - `path` — the file to read **Returns**: the contents, or the failure **Examples** ```kex FS.File.read("hello.txt") # => Ok("hello\n") FS.File.read("nowhere.txt") # => Error(ReadFailed("nowhere.txt")) ``` _Reading with a default_ ```kex let config = FS.File.read("app.conf").or("") ``` _Counting words in a file_ ```kex FS.File.read("essay.txt").or("").split(" ").reject(~empty?).count ``` ### `readBytes` ```kex readBytes(path: FilePath) -> Result ``` Reads the whole file as raw bytes, without decoding it as text. The byte counterpart of `FS.File.read`: it never validates UTF-8, so it round trips images, archives, and any other non-text payload losslessly. Answers `Error(ReadFailed(path))` when the file cannot be read. **Parameters** - `path` — the file to read **Returns**: the contents, or the failure **Examples** ```kex FS.File.readBytes("logo.png") # => Ok(#Binary<2048 bytes>) ``` _Hashing a file_ ```kex FS.File.readBytes(path).map { |data| Digest.sha256(data).hex } ``` ### `writeBytes` ```kex writeBytes(path: FilePath, content: Binary) -> Result ``` Writes `content` to `path` as raw bytes, replacing whatever was there. The byte counterpart of `FS.File.write`: the payload lands on disk exactly as given, with no encoding step. Answers `Error(WriteFailed(path))` when the write fails. **Parameters** - `path` — the file to write - `content` — the bytes to write **Returns**: `Ok` when the write succeeded, or the failure **Examples** ```kex FS.File.writeBytes("out.bin", Binary.fromBytes([0, 255])).ok? # => true ``` _Copying a file without decoding it_ ```kex FS.File.readBytes(source).flatMap { |data| FS.File.writeBytes(target, data) } ``` ### `write` ```kex write(path: FilePath, content: String) -> Result ``` Writes `content` to `path`, replacing whatever was there. Creates the file if it does not exist. The containing directory must already exist: see `FS.Directory.create`. Answers `Error(WriteFailed(path))` when the write fails. **Parameters** - `path` — the file to write - `content` — the text to write **Returns**: `Ok` when the write succeeded, or the failure **Examples** ```kex FS.File.write("out.txt", "hello\n").ok? # => true ``` _Reporting a failed write_ ```kex if FS.File.write(target, rendered).error? IO.printError("could not write ${target}") end ``` _Writing a list of lines_ ```kex FS.File.write("names.txt", names.join("\n") + "\n") ``` ### `append` ```kex append(path: FilePath, content: String) -> Bool ``` Adds `content` to the end of `path`, keeping what is already there. Creates the file if it does not exist, so it is safe to append to a log that has not been started yet. **Parameters** - `path` — the file to append to - `content` — the text to add **Returns**: `true` when the write succeeded **Examples** ```kex FS.File.append("app.log", "started\n") # => true ``` _A simple logger_ ```kex foul log(message: String) -> Void do FS.File.append("app.log", "${Time.now.to(String).or("")} ${message}\n") end ``` ### `exists?` ```kex exists?(path: FilePath) -> Bool ``` Returns `true` when something exists at `path`: a file, a directory, or anything else. Use `file?` or `directory?` when the kind matters. **Parameters** - `path` — the path to test **Returns**: `true` when the path exists **Examples** ```kex FS.File.exists?("README.md") # => true FS.File.exists?("nowhere") # => false ``` _Not overwriting an existing file_ ```kex if FS.File.exists?(target) IO.printError("${target} already exists") else FS.File.write(target, content) end ``` ### `file?` ```kex file?(path: FilePath) -> Bool ``` Returns `true` when `path` exists and is a regular file: not a directory. **Parameters** - `path` — the path to test **Returns**: `true` for a regular file **Examples** ```kex FS.File.file?("README.md") # => true FS.File.file?("src") # => false ``` ### `directory?` ```kex directory?(path: FilePath) -> Bool ``` Returns `true` when `path` exists and is a directory. **Parameters** - `path` — the path to test **Returns**: `true` for a directory **Examples** ```kex FS.File.directory?("src") # => true FS.File.directory?("README.md") # => false ``` _Separating files from directories in a listing_ ```kex entries.partition { |e| FS.File.directory?(e) } ``` ### `delete` ```kex delete(path: FilePath) -> Bool ``` Deletes the file at `path`. Answers `false` when the file does not exist or cannot be removed. Use `FS.Directory.delete` for a directory. **Parameters** - `path` — the file to delete **Returns**: `true` when the file was deleted **Examples** ```kex FS.File.delete("tmp.txt") # => true ``` _Cleaning up after a temporary file_ ```kex FS.File.write(tmp, data) process(tmp) FS.File.delete(tmp) ``` ### `copy` ```kex copy(src: FilePath, dst: FilePath) -> Bool ``` Copies the file at `src` to `dst`, replacing `dst` if it exists. **Parameters** - `src` — the file to copy - `dst` — where to copy it **Returns**: `true` when the copy succeeded **Examples** ```kex FS.File.copy("app.conf", "app.conf.bak") # => true ``` _Backing a file up before rewriting it_ ```kex FS.File.copy(path, "${path}.bak") FS.File.write(path, updated) ``` ### `rename` ```kex rename(src: FilePath, dst: FilePath) -> Bool ``` Renames (or moves) the file at `src` to `dst`. **Parameters** - `src` — the current path - `dst` — the new path **Returns**: `true` when the rename succeeded **Examples** ```kex FS.File.rename("draft.txt", "final.txt") # => true ``` _Writing to a temporary file, then swapping it in_ ```kex FS.File.write("${path}.tmp", content) FS.File.rename("${path}.tmp", path) ``` ### `changePermission` ```kex changePermission(path: FilePath, permissions: [Permission]) -> Bool ``` Sets what may be done with the file at `path`, portably. The file ends up with exactly the listed permissions: anything left out is taken away. The owner gets all of them; everyone else gets the same without `Writable`. On POSIX that is `0o755` for all three, `0o644` for `[Readable, Writable]` and `0o444` for `[Readable]`. On Windows only `Writable` has an effect, as the read-only attribute: files there are always readable, and whether one runs is decided by its extension. Use `changeMode` when you need exact POSIX bits, such as a file only its owner may read. **Parameters** - `path` — the file whose permissions to change - `permissions` — everything the file should allow **Returns**: `true` when the permissions were changed **Examples** _Making a file read-only_ ```kex FS.File.changePermission("report.txt", [FS.Readable]) # => true ``` _Writing a script and running it_ ```kex FS.File.write(script, "#!/bin/sh\necho hello\n") FS.File.changePermission(script, [FS.Readable, FS.Writable, FS.Executable]) Process.run(script, []) ``` ### `changeMode` ```kex changeMode(path: FilePath, mode: Integer) -> Bool ``` Sets the POSIX permission bits of the file at `path` to `mode`: what `chmod` does with a numeric mode. `mode` is most readably written in octal: `0o600` is a file only its owner may read and write. The bits are replaced, not added to, and a mode outside `0o0000..0o7777` is refused. This is the POSIX escape hatch: on Windows only the owner-write bit means anything. Prefer `changePermission`, which says what you want rather than how POSIX spells it. **Parameters** - `path` — the file whose mode to change - `mode` — the permission bits, as in `chmod` **Returns**: `true` when the mode was changed **Examples** ```kex FS.File.changeMode("secrets.txt", 0o600) # => true ``` ### `readLines` ```kex readLines(path: FilePath) -> [String]? ``` Reads the file and returns its lines, without their newlines. Answers `None` when the file cannot be read. A trailing newline does not produce a final empty line, so the count is the number of lines you would see in an editor. NOT `lines`: FilePath is an alias for String, so a receiver function named `lines` here is indistinguishable from String's own `lines` at every call site, and merely saying `using FS` made `text.lines` ambiguous. `readLines` also says what it does: it reads the file. **Parameters** - `path` — the file to read **Returns**: the lines, or `None` **Examples** ```kex FS.File.readLines("names.txt") # => Just(["ada", "grace"]) FS.File.readLines("nowhere") # => None ``` _Ignoring blank lines and comments_ ```kex FS.File.readLines("hosts") .or([]) .map(~trim) .reject { |line| line.empty? || line.startsWith?("#") } ``` ### `feed` ```kex feed(path: FilePath) -> Feed? ``` Returns a lazy `Feed` of the file's lines, or `None` when it cannot be read. Unlike `readLines`, the file is read a line at a time off a handle held open for the feed's lifetime, so this is the way to walk a file too large to hold in memory: nothing but the current line is retained. A `Feed` rather than a `Stream` because that is what a file honestly is: reading consumes, and there is no rewinding. Taking twice walks forward rather than answering the same lines again. On a file small enough to replay, `toStream` buys that back. The feed ends at the last line, so asking for more lines than the file has answers just the lines there are: unlike `Stream.Sequence`, which is deliberately infinite. ```kex FS.File.feed("two-lines.txt").map { |lines| lines.take(5) }.or([]) # => ["one", "two"] ``` **Parameters** - `path` — the file to read **Returns**: the lines as a feed, or `None` **Examples** _Peeking at the head of a large file_ ```kex match FS.File.feed("huge.log") do Just(lines) => lines.take(10).each { |line| IO.printLine(line) } None => IO.printError("cannot read huge.log") end ``` _The first ten errors in a long log, in one pass_ ```kex FS.File.feed("app.log") .map { |lines| lines.filter { |line| line.contains?("ERROR") }.take(10) } .or([]) ``` ### `size` ```kex size(path: FilePath) -> Integer? ``` Returns the file's size in bytes, or `None` when it cannot be read. Bytes, not characters: a file of non-ASCII text has more bytes than it has characters. **Parameters** - `path` — the file to measure **Returns**: the size in bytes, or `None` **Examples** ```kex FS.File.size("hello.txt") # => Just(6) FS.File.size("nowhere") # => None ``` _Skipping files that are too large_ ```kex paths.filter { |p| FS.File.size(p).or(0) < 1048576 } ``` ### `absolute` ```kex absolute(path: FilePath) -> String? ``` Resolves `path` against the process's current directory and returns the absolute form, or `None` when it cannot be resolved. This is the one path operation that is not in `FS.Path`, because it is not lexical: it asks the process where it is. **Parameters** - `path` — the path to resolve **Returns**: the absolute path, or `None` **Examples** ```kex FS.File.absolute("src/main.kex") # => Just("/home/ada/proj/src/main.kex") ``` _Reporting a file unambiguously_ ```kex IO.printError("failed: ${FS.File.absolute(path).or(path)}") ``` ### `canonical` ```kex canonical(path: FilePath) -> Result ``` The canonical form of `path`: absolute, with every `.`, `..` and symlink resolved, like `realpath(3)`. Unlike `absolute` this reads the filesystem, so a path that does not exist (or a symlink loop) is an error. Use it to keep reads and writes inside a directory: compare the real path's prefix, and neither `../` nor a symlink can escape. **Parameters** - `path` — the path to resolve **Returns**: the canonical path, or `ReadFailed` **Examples** ```kex FS.File.canonical("/tmp/../etc") # => Ok("/private/etc") on macOS ``` ### `symlink?` ```kex symlink?(path: FilePath) -> Bool ``` Whether `path` is itself a symlink, without following it. A dangling link is still a symlink. **Parameters** - `path` — the path to inspect **Returns**: `true` for a symlink **Examples** ```kex FS.File.symlink?("current") # => true ``` ### `info` ```kex info(path: FilePath) -> Result ``` The kind, size and modification time of `path`, read in one `lstat`. One call answers what `file?`, `directory?`, `symlink?` and `size` would otherwise ask separately, which is what walking a tree or fingerprinting it for changes wants. A symlink is described as itself, not followed. **Parameters** - `path` — the path to inspect **Returns**: what the path names, or `ReadFailed` when it cannot be read **Examples** ```kex FS.File.info("hello.txt").try.size # => 6 FS.File.info("src").try.kind # => :directory FS.File.info("nowhere") # => Error(ReadFailed("nowhere")) ``` _Noticing that a file changed_ ```kex let before = FS.File.info(path).try ... let changed = FS.File.info(path).try != before ``` ## module `FS.Path` Path arithmetic: joining, splitting, normalising and comparing paths. Everything here is pure string manipulation with no filesystem access, so the answer is the same whether or not the path exists, which also means these functions can be called from ordinary pure code, unlike `FS.File`. POSIX separators only for now. ```kex FS.Path.join("src", "main.kex") # => "src/main.kex" FS.Path.extension("src/main.kex") # => ".kex" FS.Path.withExtension("src/main.kex", "beam") # => "src/main.beam" ``` ### `separator` (constant) ```kex separator : String ``` The path separator, `"/"`. **Examples** ```kex FS.Path.separator # => "/" ``` ### `join` ```kex join(a: FilePath, b: FilePath) -> String join(a: FilePath, b: FilePath) -> FilePath -> String ``` Joins two path parts with a single separator and normalises the result. Repeated separators collapse, so `join("a/", "/b")` is `"a/b"` and not `"a//b"`. An absolute second part does NOT restart the path, the way Ruby's `Pathname#join` would: use that part on its own if that is what you mean. **Parameters** - `a` — the first part - `b` — the second part **Returns**: the joined, normalised path **Examples** ```kex FS.Path.join("src", "main.kex") # => "src/main.kex" FS.Path.join("a/", "/b") # => "a/b" FS.Path.join("a/b", "../c") # => "a/c" ``` _Building a path under a base directory_ ```kex FS.Path.join(outputDir, FS.Path.basename(source)) ``` ### `joinAll` ```kex joinAll(parts: [FilePath]) -> String ``` Joins any number of path parts, skipping empty ones, and normalises the result. An empty list gives `"."`. The list form is `joinAll`, not another `join` overload: a list receiver already has `List.join`, and a second one-argument `join` on the same receiver would be indistinguishable from it. **Parameters** - `parts` — the parts to join **Returns**: the joined, normalised path **Examples** ```kex FS.Path.joinAll(["a", "b", "c.txt"]) # => "a/b/c.txt" FS.Path.joinAll(["a", "", "b"]) # => "a/b" FS.Path.joinAll([]) # => "." ``` _Rebuilding a path from its segments_ ```kex FS.Path.joinAll(FS.Path.segments(path).drop(1)) ``` ### `normalize` ```kex normalize(path: FilePath) -> String ``` Resolves `.` and `..` in a path, lexically. Because it is lexical it never follows a symlink and never touches the disk. A leading `..` in a RELATIVE path is kept: there is no way to know what it escapes to, while one in an absolute path is dropped, since `/` has no parent. **Parameters** - `path` — the path to normalise **Returns**: the normalised path **Examples** ```kex FS.Path.normalize("a/./b/../c") # => "a/c" FS.Path.normalize("../a") # => "../a" FS.Path.normalize("/a/../../b") # => "/b" ``` _Comparing two paths for equality_ ```kex FS.Path.normalize(a) == FS.Path.normalize(b) ``` ### `segments` ```kex segments(path: FilePath) -> [String] ``` Returns the non-empty parts of the normalised path. The root `/` and the current directory `.` have none. **Parameters** - `path` — the path to split **Returns**: the segments, outermost first **Examples** ```kex FS.Path.segments("/a/b/c") # => ["a", "b", "c"] FS.Path.segments("a/./b") # => ["a", "b"] FS.Path.segments("/") # => [] ``` _How deep a path is_ ```kex FS.Path.segments(path).count ``` ### `absolute?` ```kex absolute?(path: FilePath) -> Bool ``` Returns `true` when the path starts at the root. **Parameters** - `path` — the path to test **Returns**: `true` for an absolute path **Examples** ```kex FS.Path.absolute?("/etc/hosts") # => true FS.Path.absolute?("src/main") # => false ``` ### `relative?` ```kex relative?(path: FilePath) -> Bool ``` Returns `true` when the path does not start at the root. The opposite of `absolute?`. **Parameters** - `path` — the path to test **Returns**: `true` for a relative path **Examples** ```kex FS.Path.relative?("src/main") # => true FS.Path.relative?("/etc/hosts") # => false ``` ### `dirname` ```kex dirname(path: FilePath) -> String ``` Returns the path's parent directory. A child of the root has `"/"` as its parent; a bare name has `"."`, the current directory. **Parameters** - `path` — the path to take the parent of **Returns**: the parent directory **Examples** ```kex FS.Path.dirname("/a/b/c.txt") # => "/a/b" FS.Path.dirname("/c.txt") # => "/" FS.Path.dirname("c.txt") # => "." ``` _Making sure a file's directory exists before writing_ ```kex FS.Directory.create(FS.Path.dirname(target)) ``` ### `basename` ```kex basename(path: FilePath) -> String ``` Returns the last segment of the path: the file or directory name. The root itself answers `"/"`, and an empty path answers `"."`. **Parameters** - `path` — the path to take the name of **Returns**: the last segment **Examples** ```kex FS.Path.basename("/a/b/c.txt") # => "c.txt" FS.Path.basename("/a/b") # => "b" FS.Path.basename("/") # => "/" ``` _Listing names rather than paths_ ```kex paths.map { |p| FS.Path.basename(p) } ``` ### `extension` ```kex extension(path: FilePath) -> String ``` Returns the file extension, including its leading dot, or `""` when there is none. Only the last extension counts, so `"a.tar.gz"` has `".gz"`. A leading dot is part of the NAME rather than an extension, so `".gitignore"` has none. **Parameters** - `path` — the path to inspect **Returns**: the extension, or `""` **Examples** ```kex FS.Path.extension("src/main.kex") # => ".kex" FS.Path.extension("a/b.tar.gz") # => ".gz" FS.Path.extension("README") # => "" FS.Path.extension(".gitignore") # => "" ``` _Selecting sources by extension_ ```kex files.filter { |f| FS.Path.extension(f) == ".kex" } ``` ### `stem` ```kex stem(path: FilePath) -> String ``` Returns the basename with its extension removed. A name that IS its extension (`".gitignore"`) keeps it, because it has none to drop. **Parameters** - `path` — the path to inspect **Returns**: the name without its extension **Examples** ```kex FS.Path.stem("src/main.kex") # => "main" FS.Path.stem("a/b.tar.gz") # => "b.tar" FS.Path.stem(".gitignore") # => ".gitignore" ``` _Deriving a module name from a filename_ ```kex FS.Path.stem(source).capitalize ``` ### `withExtension` ```kex withExtension(path: FilePath, wanted: String) -> String ``` Returns the path with its extension replaced by `wanted`. The new extension may be written with or without its leading dot; an empty one removes the extension entirely. **Parameters** - `path` — the path to rewrite - `wanted` — the new extension, with or without its dot **Returns**: the rewritten path **Examples** ```kex FS.Path.withExtension("src/main.kex", "beam") # => "src/main.beam" FS.Path.withExtension("src/main.kex", ".beam") # => "src/main.beam" FS.Path.withExtension("src/main.kex", "") # => "src/main" ``` _Deriving an output path from a source path_ ```kex FS.Path.withExtension(FS.Path.join(outDir, FS.Path.basename(src)), "o") ``` ### `relativeTo` ```kex relativeTo(path: FilePath, base: FilePath) -> String ``` Expresses `path` relative to `base`, walking up with `..` as needed. Purely lexical, so it answers only when both sides are anchored the same way; a relative path against an absolute base, or the reverse, comes back unchanged. A path that IS the base answers `"."`. **Parameters** - `path` — the path to re-express - `base` — the directory to express it against **Returns**: the relative path **Examples** ```kex FS.Path.relativeTo("/a/b/c.txt", "/a/d") # => "../b/c.txt" FS.Path.relativeTo("/a/b/c.txt", "/a") # => "b/c.txt" FS.Path.relativeTo("/a/b", "/a/b") # => "." ``` _Printing project-relative paths in a report_ ```kex matches.each { |p| IO.printLine(FS.Path.relativeTo(p, projectRoot)) } ``` ## module `FS.Directory` Creating, listing and removing directories. Like `FS.File`, everything here reaches the real filesystem and so is `foul`. Listings answer with an `Optional`, so a directory that cannot be read is a value you handle rather than an exception. ### `exists?` ```kex exists?(path: FilePath) -> Bool ``` Returns `true` when something exists at `path`. **Parameters** - `path` — the path to test **Returns**: `true` when the path exists **Examples** ```kex FS.Directory.exists?("src") # => true ``` ### `directory?` ```kex directory?(path: FilePath) -> Bool ``` Returns `true` when `path` exists and is a directory. **Parameters** - `path` — the path to test **Returns**: `true` for a directory **Examples** ```kex FS.Directory.directory?("src") # => true FS.Directory.directory?("README.md") # => false ``` ### `file?` ```kex file?(path: FilePath) -> Bool ``` Returns `true` when `path` exists and is a regular file. **Parameters** - `path` — the path to test **Returns**: `true` for a regular file **Examples** ```kex FS.Directory.file?("README.md") # => true ``` ### `create` ```kex create(path: FilePath) -> Bool ``` Creates the directory at `path`. **Parameters** - `path` — the directory to create **Returns**: `true` when the directory was created **Examples** ```kex FS.Directory.create("build") # => true ``` _Making sure an output directory is there_ ```kex FS.Directory.create(outDir) if !FS.Directory.exists?(outDir) ``` ### `delete` ```kex delete(path: FilePath) -> Bool ``` Removes the directory at `path`, which must be empty. Use `deleteAll` to remove a directory together with its contents. **Parameters** - `path` — the directory to remove **Returns**: `true` when the directory was removed **Examples** ```kex FS.Directory.delete("empty") # => true ``` ### `deleteAll` ```kex deleteAll(path: FilePath) -> Bool ``` Removes the directory at `path` and everything inside it, recursively. This deletes data and cannot be undone: check the path before calling it, particularly when it was computed or came from user input. **Parameters** - `path` — the directory tree to remove **Returns**: `true` when the tree was removed **Examples** ```kex FS.Directory.deleteAll("build") # => true ``` _Rebuilding a scratch directory from clean_ ```kex FS.Directory.deleteAll(tmpDir) FS.Directory.create(tmpDir) ``` ### `list` ```kex list(path: FilePath) -> [String]? ``` Lists the names in `path`: both files and directories, one level deep. The results are bare names, not paths; join them with `path` to get something you can open. Answers `None` when the directory cannot be read. **Parameters** - `path` — the directory to list **Returns**: the entry names, or `None` **Examples** ```kex FS.Directory.list("src") # => Just(["main.kex", "lexer"]) ``` _Turning names into usable paths_ ```kex FS.Directory.list(dir).or([]).map { |name| FS.Path.join(dir, name) } ``` ### `files` ```kex files(path: FilePath) -> [String]? ``` Lists only the regular files in `path`, one level deep. **Parameters** - `path` — the directory to list **Returns**: the file names, or `None` **Examples** ```kex FS.Directory.files("src") # => Just(["main.kex"]) ``` _Every Kex source in a directory_ ```kex FS.Directory.files(dir) .or([]) .filter { |f| FS.Path.extension(f) == ".kex" } ``` ### `directories` ```kex directories(path: FilePath) -> [String]? ``` Lists only the subdirectories of `path`, one level deep. **Parameters** - `path` — the directory to list **Returns**: the directory names, or `None` **Examples** ```kex FS.Directory.directories("src") # => Just(["lexer", "parser"]) ``` _Walking one level down_ ```kex FS.Directory.directories(root).or([]).each do |name| IO.printLine(FS.Path.join(root, name)) end ``` ### `current` ```kex current : String ``` Returns the process's current working directory, as an absolute path. **Returns**: the current directory **Examples** ```kex FS.Directory.current # => "/home/ada/project" ``` _Resolving a relative path by hand_ ```kex FS.Path.join(FS.Directory.current, "build") ``` ### `home` ```kex home : String? ``` Returns the current user's home directory, or `None` when it cannot be determined. **Returns**: the home directory, or `None` **Examples** ```kex FS.Directory.home # => Just("/home/ada") ``` _A per-user config path_ ```kex FS.Path.join(FS.Directory.home.or("."), ".apprc") ``` ### `temporary` ```kex temporary : String ``` Returns the directory this system puts temporary files in. Total, unlike `home`: it answers `TMPDIR` when the environment sets one (`TEMP` or `TMP` on Windows) and falls back to `/tmp`, so there is always somewhere to write. The path never ends in a separator, so it composes with `FS.Path.join` directly. The directory is shared with every other process on the machine, so pick a name unlikely to collide and delete it when you are done. **Returns**: the temporary directory **Examples** ```kex FS.Directory.temporary # => "/tmp" ``` _A scratch file that cleans up after itself_ ```kex let scratch = FS.Path.join(FS.Directory.temporary, "report.bin") FS.File.writeBytes(scratch, payload) FS.File.delete(scratch) ``` ### `createTemporary` ```kex createTemporary(prefix: String) -> FilePath? ``` Creates a fresh, empty directory under `temporary` and returns its path. The name is `prefix` followed by a random suffix, so concurrent callers using the same prefix each get their own directory. Nothing removes it for you: `deleteAll` it when you are done. **Parameters** - `prefix` — the start of the directory name, to say whose it is **Returns**: the new directory, or `None` when it could not be created **Examples** ```kex FS.Directory.createTemporary("report") # => Just("/tmp/report-9f2c41d07ab35e68") ``` _A scratch directory that cleans up after itself_ ```kex let scratch = FS.Directory.createTemporary("build").or(die("no temporary directory")) FS.File.write(FS.Path.join(scratch, "out.txt"), text) FS.Directory.deleteAll(scratch) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: io.kex title: IO entities: - { kind: module, name: "IO" } --- # IO ## module `IO` Console input and output. `IO` is a capability: every function in it touches the outside world, so it can only be called from `foul` code (or from `main`). Reading a line and printing a line are the two workhorses; `inspect` is the debugging tool that can be dropped into the middle of a chain without changing its value. ```kex main do IO.print("name? ") let name = IO.getLine.or("world") IO.printLine("hello, ${name.trim}") end ``` ### `printLine` ```kex printLine(msg: Showable) -> Void ``` Writes `msg` to stdout followed by a newline. Any `Showable` value is accepted, not just strings: numbers, lists, maps and records print through their own `show` implementation. Called with no argument it prints an empty line. **Parameters** - `msg` — the value to write **Examples** _Printing values of different types_ ```kex IO.printLine("hello") # prints: hello IO.printLine(42) # prints: 42 IO.printLine([1, 2, 3]) # prints: [1, 2, 3] IO.printLine # prints an empty line ``` _Interpolation is usually clearer than several arguments_ ```kex let user = "ada" IO.printLine("logged in as ${user}") # prints: logged in as ada ``` ### `print` ```kex print(msg: Showable) -> Void ``` Writes `msg` to stdout without a trailing newline. Use it to build a line from several pieces, or to write a prompt that the cursor should stay on. **Parameters** - `msg` — the value to write **Examples** _Building one line from several writes_ ```kex IO.print("hello ") IO.print("world") IO.printLine # stdout: hello world ``` _A prompt the answer is typed next to_ ```kex IO.print("continue? [y/N] ") let answer = IO.getLine.or("n") ``` ### `inspect` ```kex inspect(val: A) -> A ``` Writes a colored, structured rendering of `val` to stderr and returns `val` unchanged. Because it returns its argument, `inspect` can be spliced into the middle of a chain to see what is flowing through it, then removed again without touching the surrounding code. It writes to stderr, so it does not disturb a program whose stdout is piped somewhere. The type checker treats it as pure, so it is allowed inside pure functions. Use `inspected` instead when you want the rendering as a `String` rather than written out. **Parameters** - `val` — any inspectable value **Returns**: the same value, unchanged **Examples** _Watching an intermediate step of a chain_ ```kex [1, 2, 3, 4] .map { |n| n * 3 } .inspect # stderr: [3, 6, 9, 12] : [Int] .filter(~even?) # => [6, 12] ``` _The rendering carries the value's type_ ```kex IO.inspect("hi") # stderr: "hi" : String IO.inspect((1..4)) # stderr: 1..4 : Range ``` ### `getLine` ```kex getLine : String? ``` Reads one line from stdin, without the trailing newline. Returns `None` at end of input, which is what makes it usable as a loop condition: the `None` is the end of the stream, not an error. **Returns**: the line, or `None` at end of input **Examples** _Reading an answer with a default_ ```kex let answer = IO.getLine.or("") ``` _Draining stdin line by line_ ```kex foul echoAll -> Void do match IO.getLine do Just(line) => do IO.printLine(line) echoAll end None => () end end ``` ### `get` ```kex get : String? ``` Reads a single character from stdin. Returns `None` at end of input. Note that the result is a one-character `String`, not a `Char`. **Returns**: the character, or `None` at end of input **Examples** ```kex let key = IO.get.or("") IO.printLine("you pressed ${key}") ``` ### `printError` ```kex printError(msg: Showable) -> Void ``` Writes `msg` to stderr followed by a newline. Diagnostics belong on stderr so that a program's real output can be piped or redirected on its own. Unlike a raised error, this only prints: it does not stop the program. **Parameters** - `msg` — the message to write **Examples** _Reporting a problem without exiting_ ```kex IO.printError("config file not found: using defaults") ``` _Keeping stdout clean for the real result_ ```kex IO.printError("scanning ${dir}...") # progress, on stderr IO.printLine(results.join("\n")) # the output, on stdout ``` ### `warn` ```kex warn(msg: Showable) -> Void ``` Writes `msg` to stderr. Identical to `printError`, named for the case where the message is a warning rather than a failure. **Parameters** - `msg` — the warning to write **Examples** ```kex IO.warn("ignoring unknown key ${key}") ``` ### `warning` ```kex warning(msg: Showable) -> Void ``` Writes `msg` to stderr. The long spelling of `warn`. **Parameters** - `msg` — the warning to write **Examples** ```kex IO.warning("this option is deprecated") ``` ### `out` (constant) ```kex out : FileHandle ``` The three standard streams, as ordinary handle VALUES. `IO.printLine(x)` and `IO.out.printLine(x)` both write to standard output. Pass a handle to a function that accepts a sink to choose where it writes. Typestate says what each one permits: writing to `IO.in`, or reading from `IO.out`, is a compile error, exactly as it is for a file opened `Read`. Obtaining a standard-stream handle is pure; reading or writing through it is `foul`. `Mock.IO` captures writes through both `IO.out` and `IO.printLine`. A `with IO = ...` override replaces module calls only. **Examples** _Handing a library somewhere to write_ ```kex foul report(out: Writable, lines: [String]) -> Void do lines.each { |line| out.printLine(line) } end report(IO.out, results) report(IO.error, warnings) report(FS.File.open("report.txt", Write).try, results) ``` ### `error` (constant) ```kex error : FileHandle ``` Standard error, as a handle. The sink `IO.printError` and `IO.warn` write to, reachable as a value. **Examples** ```kex IO.error.printLine("config file not found") ``` ### `in` (constant) ```kex in : FileHandle ``` Standard input, as a handle. The source `IO.getLine` and `IO.get` read from, reachable as a value. **Examples** _Reading from a file or from a pipe, with one function_ ```kex foul firstLine(source: Readable) -> String do source.getLine.or("(empty)") end firstLine(IO.in) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: json.kex title: JSON entities: - { kind: module, name: "JSON" } --- # JSON ## module `JSON` ### `parse` ```kex parse(text: String) -> Result parse(text: String, options: {Atom: Bool}) -> Result ``` Parses a JSON document, strictly. The whole text must be one JSON value with nothing after it: trailing input is `TrailingInput`, not a silently ignored tail. Objects come back as maps with atom keys, arrays as lists, `null` as `None`. **Parameters** - `text` — the JSON document **Returns**: the parsed value, or why it failed **Examples** ```kex JSON.parse("{\"a\": 1, \"b\": [true, null, 2.5]}") # => Ok({ a: 1, b: [true, None, 2.5] }) JSON.parse("[1, 2") # => Error(UnexpectedEnd(5)) ``` _Reading a config file_ ```kex match JSON.parse(FS.File.read("config.json").or("")) do Ok(config) => IO.printLine(config) Error(e) => IO.printError("config.json is not valid JSON: ${e}") end ``` ### `stringify` ```kex stringify(value: Any) -> String ``` Renders a Kex value as strict JSON text. Maps become objects, lists become arrays, `None` becomes `null`, and strings are escaped. A map written with atom keys (the usual Kex spelling) renders with those names as strings, so `{ name: "Ada" }` becomes `{"name":"Ada"}`. Object keys come out in canonical key order. Anything the encoder does not recognise renders as `null` rather than failing, so this never raises. **Parameters** - `value` — the value to render **Returns**: the JSON text **Examples** ```kex JSON.stringify({ name: "Ada", n: 1, ok: true }) # => '{"n":1,"name":"Ada","ok":true}' JSON.stringify([1, 2, 3]) # => '[1,2,3]' ``` _Writing a JSON file_ ```kex FS.File.write("out.json", JSON.stringify(report)) ``` _A round trip_ ```kex JSON.parse(JSON.stringify({ a: 1 })) # => Ok({ a: 1 }) ``` ## type `Error` Why a document could not be parsed. Every variant carries the position in the input where the parser stopped, so a caller can point at the problem. ```kex JSON.parse("[1, 2") # => Error(UnexpectedEnd(5)) JSON.parse("// c\n1") # => Error(UnexpectedCharacter("/", 0)) ``` **Variants** - `UnexpectedCharacter(String, Integer)` - `UnexpectedEnd(Integer)` - `InvalidLiteral(String, Integer)` - `InvalidNumber(String, Integer)` - `InvalidEscape(String, Integer)` - `InvalidUnicodeEscape(String, Integer)` - `UnterminatedComment(Integer)` - `TrailingInput(Integer)` - `UnknownOption(Atom)` --- --- package: prelude version: "0.4.0-rc.3-dev" source: kex.kex title: Kex entities: - { kind: trait, name: "Inspectable" } - { kind: trait, name: "Showable" } - { kind: make, name: "Inspectable" } - { kind: make, name: "Showable" } - { kind: make, name: "Optional" } - { kind: make, name: "Result" } - { kind: module, name: "Kex" } - { kind: module, name: "Kex.Interface" } - { kind: function, name: "inspected" } - { kind: function, name: "inspect" } --- # Kex ## trait `Inspectable` Types that can be rendered structurally, for a person reading output. The rendering shows the value's STRUCTURE: quotes on strings, `Just(...)` around an optional, which is what makes it right for debugging and wrong for user-facing text. `Showable` is the other half of that pair. Every type is inspectable through a structural fallback, so `inspected` and `IO.inspect` work on anything; a type that wants a different rendering overrides `inspectValue`. ```kex [1, 2].inspected # => "[1, 2]" Just("hi").inspected # => "Just(\"hi\")" ``` Implemented by [`Binary`](binary.md#make-binary), [`Period`](time.md#make-period), [`Date`](time.md#make-date), [`Time`](time.md#make-time), [`DateTime`](time.md#make-datetime), [`URI`](uri.md#make-uri), [`URL`](uri.md#make-url), [`Headers`](net/http.md#make-headers). ### Provided functions #### `inspectValue` ```kex inspectValue(colors: Bool) -> String ``` Renders the value structurally, with ANSI colors when `colors` is true. `inspected` and `IO.inspect` call this for you, passing the console's own color setting: call it directly only when you need to force one. **Parameters** - `colors` — whether to include ANSI color escapes **Returns**: the rendered value **Examples** ```kex [1, 2].inspectValue(false) # => "[1, 2]" ``` ## trait `Showable` Types that have a concise, user-facing text representation. `Showable` is used by interpolation, printing, and `to(String)`. Its output should describe the value itself rather than its implementation structure: a string is shown without quotes, `Just(x)` is shown as `x`, and `None` is shown as an empty string. Use `Inspectable` when debugging structure matters. ```kex "hello".showValue # => "hello" Just("hello").showValue # => "hello" None.showValue # => "" ``` Implemented by [`Binary`](binary.md#make-binary), [`Optional`](#make-optional-showable), [`Result`](#make-result), [`Period`](time.md#make-period), [`Date`](time.md#make-date), [`Time`](time.md#make-time), [`DateTime`](time.md#make-datetime), [`Measure`](units.md#make-measure), [`URI`](uri.md#make-uri), [`URL`](uri.md#make-url), [`Queue`](data/queue.md#make-queue), [`Set`](data/set.md#make-set), [`UnorderedSet`](data/set.md#make-unorderedset), [`Stack`](data/stack.md#make-stack), [`Headers`](net/http.md#make-headers). ### Provided functions #### `showValue` ```kex showValue : String ``` Returns the value's user-facing text representation. Implement this for domain types whose useful presentation differs from their structural rendering. Keep the result free of ANSI styling so it is safe in files, logs, and interpolation as well as on a terminal. **Returns**: the display text #### `to` ```kex to(_) ``` ## extends `Optional` More functions for [`Optional`](optional.md#type-optional), added by this module. Implements [`Showable`](#trait-showable). ### `showValue` (from Showable) ```kex showValue(_) ``` ## extends `Result` More functions for [`Result`](optional.md#type-result), added by this module. Implements [`Showable`](#trait-showable). The same for Result, with one deliberate asymmetry: SHOWING a result shows what it carries, so `Ok(42)` reads as `42` rather than leaking the wrapper into text, but an `Error` keeps its marker. Dropping it made a failure and a success print identically, and unlike `None` (which shows as "", visibly not a value) there would be nothing left to tell them apart. The marker is spelled `Error(...)`, matching how the same value renders as an ELEMENT of a collection: `[1, Error(Bad(no))]`. Elements render structurally rather than through showValue (as in Ruby, where `nil.to_s` is "" but `[nil].to_s` is "[nil]"), so this is the one spelling that reads the same at both levels. What functions RETURN is unchanged: `Integer.parse` still answers with a Result, `IO.inspect` still shows `Ok(42)`, and both arms are still matchable. ### `showValue` (from Showable) ```kex showValue(_) ``` ## module `Kex` The running toolchain, which backend, which version, which features. ```kex Kex.BACKEND # => Interpreter Kex.BACKEND.compiled? # => false Kex.VERSION.release # => "0.4.0" Kex.Feature.has?(Kex.FileSystem) # => true ``` ### `BACKEND` (constant) ```kex BACKEND : Backend ``` Which backend is executing this program. Its `interpreted?`, `compiled?` and `beam?` are the readable way to ask. **Examples** ```kex Kex.BACKEND # => Interpreter ``` ### `VERSION` (constant) ```kex VERSION : Version ``` This build's version. **Examples** ```kex Kex.VERSION.major # => 0 Kex.VERSION.release # => "0.4.0-alpha.2" ``` _Reporting the version in a tool's output_ ```kex IO.printLine("built with Kex ${Kex.VERSION.number}") ``` ### `load` ```kex load(path: String) -> Result ``` Loads the compiled module at `path`, replacing an older version of it that is already loaded. See `Kex.LoadedModule`. **Parameters** - `path` — a `.beam` file written by `kex --compile` **Returns**: the module, or why it could not be loaded **Examples** ```kex let theme = Kex.load("build/kex_theme.beam").try ``` ### `loaded` ```kex loaded(name: Atom) -> LoadedModule? ``` The module already loaded under `name`, or `None` when there is none. **Parameters** - `name` — the module's BEAM name **Returns**: the module **Examples** _Loading only once_ ```kex let theme = Kex.loaded(:kex_theme).or(Kex.load(path).try) ``` ### `hash` ```kex hash(value: Any) -> Integer ``` A non-cryptographic hash of any value: a non-negative `Integer` below 2^32, the same for equal values. Cheap to compute over a whole structure, with no string built along the way, which suits fingerprinting data to notice when it changed. It is stable only within one running program: the number differs between the interpreter and the BEAM, and may change between Kex versions, so never store it or send it anywhere. Use `Digest` for that, or for anything an adversary could choose collisions for. **Parameters** - `value` — the value to hash **Returns**: the hash, from 0 up to 2^32 - 1 **Examples** _Noticing that a project's files changed_ ```kex let stamp = Kex.hash(paths.map { |path| (path, FS.File.info(path).try) }) ``` ## type `Backend` Which backend is executing the program: the tree-walking `Interpreter`, or the `Beam` virtual machine. **Variants** - `Interpreter` - `Beam` ### Functions #### `interpreted?` ```kex interpreted? : Bool ``` Whether this is the tree-walking interpreter. **Returns**: `true` for `Interpreter` **Examples** ```kex Kex.BACKEND.interpreted? # => true under `kex -R file.kex` ``` #### `compiled?` ```kex compiled? : Bool ``` Whether this backend runs compiled code: today, the BEAM. **Returns**: `true` for every backend but the interpreter **Examples** ```kex Kex.BACKEND.compiled? # => true under `kex file.kex` ``` #### `beam?` ```kex beam? : Bool ``` Whether this is the BEAM virtual machine. Processes, the web server and clustering need it. **Returns**: `true` for `Beam` **Examples** ```kex Kex.BACKEND.beam? # => true under `kex file.kex` ``` ## type `Feature` An optional capability a build may or may not include. Ask about one with `Kex.Feature.has?` before relying on it. `FileSystem`: the program can read and write the host's files (`FS`). `ExternalPrograms`: it can run other programs (`Process.run`, `Process.stream`), which the browser build cannot. Kex's own processes (`spawn`, `receive`) are not optional: every backend has them. Networking has its finer-grained report, `Net.Support.current`. **Variants** - `FileSystem` - `ExternalPrograms` ## record `Version` Build identity for the compiler and runtime executing this program: `Version` and `VERSION` below. Useful in bug reports, generated artifacts, and compatibility checks where `Kex.BACKEND` alone is not enough to identify the toolchain. The toolchain a program is running on. `kex --version` and the REPL banner report the same numbers. `revision` is the git commit the compiler was built from: `None` when it was built from a source archive rather than a checkout, which is why it is an Optional rather than a String. **Examples** ```kex Kex.VERSION.major # => 0 Kex.VERSION.revision # => Just("a1b2c3d") Kex.VERSION.to(String) # => "0.3.0 (a1b2c3d)" ``` **Fields** - `major` : [Integer](number.md#make-integer) - `minor` : [Integer](number.md#make-integer) - `patch` : [Integer](number.md#make-integer) - `revision` : [String](string.md#make-string)? - `preRelease` : [String](string.md#make-string) (optional) ### Functions #### `tuple` ```kex tuple : (Integer, Integer, Integer, String?) ``` The version's four values as a tuple, for destructuring. A tuple cannot carry accessors of its own: there is no named type for a `make` block to target, so the record is the value and this is the view. **Returns**: major, minor, patch, revision **Examples** ```kex let (major, minor, patch, revision) = Kex.VERSION.tuple major # => 0 ``` #### `release` ```kex release : String ``` The three numbers, plus the pre-release channel when there is one. `0.4.0`, or `0.4.0-rc.1` on a pre-release build. What a version RANGE is matched against, so the channel has to be in it. **Returns**: the release string **Examples** ```kex Kex.VERSION.release # => "0.4.0-alpha.2" ``` #### `number` ```kex number : String ``` The release string with the build revision after it, when there is one. This is what `kex --version` and the REPL banner print. **Returns**: the full version string **Examples** ```kex Kex.VERSION.number # => "0.4.0-alpha.2 (219e625)" ``` _Reporting the toolchain in a tool's output_ ```kex IO.printLine("built with Kex ${Kex.VERSION.number}") ``` ## record `LoadedModule` A compiled module loaded into the running program by `Kex.load`. For a program that decides at run time what code it needs: a site generator rendering a theme's templates, a plugin host. Compile the source with `kex --compile -o ` once, then load the `.beam` and call it as often as needed in the same running VM. Every entry module is named after its file (`kex_.beam` for `.kex`), and so is everything declared at the top level of that file. A `let render = Template.html(Kex.embed(path))` written outside any `module` therefore lands in `kex_`, not in a module the file names; give each generated file a distinct name, or put the declaration inside a `module`, whose functions compile into `Kex.`. The BEAM backend only: under the interpreter, `Kex.load` answers an error. ```kex let theme = Kex.load("cache/kex_theme_3f2a.beam").try let html = theme.call(:render, [context]).try ``` **Fields** - `name` : [Atom](atom.md#make-atom) ### Functions #### `call` ```kex call(function: Atom, arguments: [Any]) -> Result ``` Calls the module's function `function` with `arguments`. A failure inside the call is an `Error` describing it, not a crash of the caller, and so is a function the module does not export with that many arguments. A `foul` function takes one argument more than it declares, its capabilities, so call pure functions this way. **Parameters** - `function` — the function's name - `arguments` — its arguments, in order **Returns**: what the function returned, or why the call failed **Examples** ```kex theme.call(:render, [context]) # => Ok("...") ``` ## module `Kex.Feature` Which optional capabilities this build includes. Optional non-network capabilities in this build. Networking has its own granular opt-in `Net.Support` report. ### `has?` ```kex has?(f: Feature) -> Bool ``` Returns `true` when this build includes `f`. **Parameters** - `f` — the capability to ask about **Returns**: `true` when it is available **Examples** ```kex Kex.Feature.has?(Kex.ExternalPrograms) # => true, except in a browser ``` ### `list` (constant) ```kex list : [Feature] ``` Every optional capability this build includes. **Examples** ```kex Kex.Feature.list # => [FileSystem, ExternalPrograms] ``` ## module `Kex.Interface` Reading the typed public surface of a compiled Kex module. ### `read` ```kex read(path: FS.FilePath) -> Any? ``` Reads the KexI interface chunk of a compiled Kex module: its typed public surface, and answers the decoded term, or None when the file has no such chunk, does not exist, or is not a BEAM artifact. The term is an ordinary tree of tuples, lists, atoms, integers and strings, so it is walked with normal pattern matching and `Tuple.items`. This exists so that reading it needs no `BEAM.*` interop: it is the one intentional entry point rather than a general term decoder. **Parameters** - `path` — the compiled .beam file to read **Returns**: the decoded interface term, or `None` **Examples** ```kex Kex.Interface.read("build/runtime/beam/kex_prelude.beam") ``` ## function `inspected` ```kex inspected(value: Inspectable) -> String ``` Returns the pretty-printed representation of any value as a STRING: the same form the REPL echoes. Universal: reachable on every type through UFCS. Use `inspected` to obtain text, and `IO.inspect` to print a value and return it unchanged in a pipeline. **Parameters** - `value` — any value **Returns**: its rendered form **Examples** ```kex [1, 2].inspected # => "[1, 2]" { a: 1 }.inspected # => "{ a: 1 }" { "a": 1 }.inspected # => "{ \"a\": 1 }" Just("hi").inspected # => "Just(\"hi\")" ``` _Putting a rendering into a message_ ```kex IO.printError("unexpected value: ${value.inspected}") ``` ## function `inspect` ```kex inspect(value: A) -> A ``` Prints the pretty-printed form of `value` to stderr and returns `value` unchanged, so it can be dropped into any pipeline without changing what flows through it. The same operation as `IO.inspect`, reachable by UFCS. **Parameters** - `value` — the value to show **Returns**: the same value, unchanged **Examples** ```kex [1, 2, 3, 4].map { |n| n * 3 }.inspect.filter(~even?) # stderr: [3, 6, 9, 12] : [Int] # => [6, 12] ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: list.kex title: List entities: - { kind: type, name: "List" } - { kind: make, name: "[Number]" } - { kind: make, name: "[[Y]]" } - { kind: make, name: "[X]" } --- # List ## type `List` An ordered, immutable sequence, written [1, 2, 3]. Lists are the default collection in Kex. Every operation answers with a new list rather than changing the receiver, so a chain of transformations is safe to read from either end. ```kex let scores = [7, 2, 9, 4] scores.filter { |n| n > 3 } # => [7, 9, 4] scores.sort # => [2, 4, 7, 9] scores.map { |n| n * 10 }.sum # => 220 ``` Anything that might not be there: the first element, an element at an index, a search result: answers with an `Optional`, so an empty list is an ordinary case rather than a crash: ```kex [].first.or(0) # => 0 [1, 2].at(9).or(0) # => 0 ``` A list is `Enumerable` and `Foldable`, which is where `map`, `filter`, `find`, `all?` and `reduce` come from. **Examples** _Building a report line by line_ ```kex users .filter { |u| u.active? } .map { |u| "${u.name} <${u.email}>" } .join("\n") ``` Implements [`Monoid`](algebra.md#trait-monoid), [`Blankable`](blankable.md#trait-blankable), [`Enumerable`](enumerable.md#trait-enumerable), [`Foldable`](enumerable.md#trait-foldable), [`Truthyable`](truthyable.md#trait-truthyable). ### Functions #### `first` ```kex first : X? ``` Returns the first element wrapped in `Just`, or `None` if the list is empty. **Examples** ```kex [1, 2, 3].first # => Just(1) [].first # => None ``` #### `second` ```kex second : X? ``` Returns the second element wrapped in `Just`, or `None` if the list has fewer than two elements. **Examples** ```kex [1, 2, 3].second # => Just(2) [1].second # => None ``` #### `third` ```kex third : X? ``` Returns the third element wrapped in `Just`, or `None` if the list has fewer than three elements. **Examples** ```kex [1, 2, 3].third # => Just(3) [1, 2].third # => None ``` #### `rest` ```kex rest : [X] ``` Returns all elements after the first. Returns `[]` for an empty or single-element list. **Examples** ```kex [1, 2, 3].rest # => [2, 3] [1].rest # => [] [].rest # => [] ``` #### `last` ```kex last : X? ``` Returns the last element wrapped in `Just`, or `None` if the list is empty. **Examples** ```kex [1, 2, 3].last # => Just(3) [].last # => None ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of elements. When given a predicate, returns the count of elements for which it holds. **Examples** ```kex [1, 2, 3].count # => 3 [1, 2, 3, 4].count(~even?) # => 2 ``` ```kex count(pred: (X -> Bool)) -> Integer ``` Returns how many elements satisfy `pred`. count(pred) is provided by the Enumerable trait. **Parameters** - `pred` — the test applied to each element **Returns**: the number of matches **Examples** ```kex [1, 2, 3, 4].count(~even?) # => 2 ["a", "", "b"].count { |s| s.empty? } # => 1 ``` #### `length` ```kex length : Integer ``` Returns the number of elements. The same as `count`, under the name `String` uses. **Returns**: the number of elements **Examples** ```kex [1, 2, 3].length # => 3 ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` if the list contains no elements. **Examples** ```kex [].empty? # => true [1, 2].empty? # => false ``` #### `find` (from Foldable) ```kex find(pred: (X -> Bool)) -> X? ``` Returns the first element satisfying the predicate wrapped in `Just`, or `None` if no element matches. find/any?/all? are provided by the Enumerable trait. **Examples** ```kex [1, 2, 3].find { |x| x > 1 } # => Just(2) [1, 2, 3].find { |x| x > 9 } # => None ``` #### `any?` (from Foldable) ```kex any?(pred: (X -> Bool)) -> Bool ``` Returns `true` if at least one element satisfies the predicate. **Examples** ```kex [1, 2, 3].any? { |x| x > 2 } # => true [1, 2, 3].any? { |x| x > 9 } # => false ``` #### `all?` (from Foldable) ```kex all?(pred: (X -> Bool)) -> Bool ``` Returns `true` if every element satisfies the predicate. **Examples** ```kex [1, 2, 3].all? { |x| x > 0 } # => true [1, 2, 3].all? { |x| x > 1 } # => false ``` #### `map` (from Enumerable) ```kex map(f: (X -> Y)) -> [Y] ``` Transforms each element by applying `f`. map/filter/each are provided by the Enumerable trait (in terms of reduce). **Examples** ```kex [1, 2, 3].map { |x| x * 2 } # => [2, 4, 6] ``` #### `filter` (from Enumerable) ```kex filter(pred: (X -> Bool)) -> [X] ``` Returns a new list containing only the elements for which `pred` is `true`. **Examples** ```kex [1, 2, 3, 4].filter { |x| x.even? } # => [2, 4] ``` #### `reject` ```kex reject(pred: (X -> Bool)) -> [X] ``` Returns a new list with all elements for which `pred` is `true` removed. The inverse of `filter`. **Examples** ```kex [1, 2, 3, 4].reject { |x| x.even? } # => [1, 3] ``` #### `each` (from Foldable) ```kex each(f: (X -> Void)) -> Void ``` Calls `f` with each element for its side effects. Returns unit. **Examples** ```kex [1, 2, 3].each { |x| IO.printLine(x) } ``` #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: A, f: (A -> X -> A)) -> A ``` Folds the list from the left, starting with `acc` and combining each element via `f`. **Parameters** - `acc` — initial accumulator value **Examples** ```kex [1, 2, 3].reduce(0) { |acc, x| acc + x } # => 6 [1, 2, 3].reduce(1) { |acc, x| acc * x } # => 6 ``` #### `flatMap` (from Enumerable) ```kex flatMap(f: (X -> [Y])) -> [Y] ``` Maps each element to a list and concatenates the results. **Examples** ```kex [1, 2, 3].flatMap { |n| [n, n * 10] } # => [1, 10, 2, 20, 3, 30] ``` #### `at` ```kex at(i: Integer) -> X? ``` Returns the element at position `i` (0-based) wrapped in `Just`, or `None` if the index is out of range. **Examples** ```kex [10, 20, 30].at(1) # => Just(20) [10, 20, 30].at(9) # => None ``` #### `get` ```kex get(i: Integer) -> X? get(i: Integer, default: X) -> X ``` Returns the element at position `i` (0-based), or `None` when the index is out of range. The same as `at`. **Parameters** - `i` — the 0-based index **Returns**: the element, or `None` **Examples** ```kex [10, 20, 30].get(1) # => Just(20) [10, 20, 30].get(9) # => None ``` #### `contains?` ```kex contains?(elem: X) -> Bool ``` Returns `true` if `elem` is present in the list. **Examples** ```kex [1, 2, 3].contains?(2) # => true [1, 2, 3].contains?(9) # => false ``` #### `indexOf` ```kex indexOf(elem: X) -> Integer? ``` Returns `true` if the first index at which `elem` appears, wrapped in `Just`, or `None` if the element is not present. **Examples** ```kex [10, 20, 30].indexOf(20) # => Just(1) [10, 20, 30].indexOf(99) # => None ``` #### `findIndex` ```kex findIndex(pred: (X -> Bool)) -> Integer? ``` Returns the index of the first element satisfying `pred`, wrapped in `Just`, or `None` if none does. The predicate counterpart of `indexOf`, which searches by value. **Examples** ```kex [10, 25, 30].findIndex { |n| n > 20 } # => Just(1) [10, 25, 30].findIndex { |n| n > 99 } # => None ``` #### `takeWhile` ```kex takeWhile(pred: (X -> Bool)) -> [X] ``` Returns the longest leading run of elements satisfying `pred`. Stops at the first element that does not, so it is not `filter`: later matches are dropped with everything after the first failure. **Examples** ```kex [1, 2, 9, 1].takeWhile { |n| n < 5 } # => [1, 2] ``` #### `dropWhile` ```kex dropWhile(pred: (X -> Bool)) -> [X] ``` Returns what is left after `takeWhile`: everything from the first element that does not satisfy `pred` onwards. **Examples** ```kex [1, 2, 9, 1].dropWhile { |n| n < 5 } # => [9, 1] ``` #### `uniq` ```kex uniq : [X] ``` Returns a new list with duplicate elements removed, preserving the first occurrence of each element. **Examples** ```kex [1, 2, 3, 2, 1].uniq # => [1, 2, 3] ``` #### `partition` ```kex partition(pred: (X -> Bool)) -> ([X], [X]) ``` Splits the list into two lists: those for which `pred` is `true` (first) and those for which it is `false` (second). **Examples** ```kex [1, 2, 3, 4].partition { |n| n.even? } # => ([2, 4], [1, 3]) ``` #### `collect` (from Enumerable) ```kex collect(f: (X -> Y?)) -> [Y] ``` Maps each element through `f` (which returns an `Optional`), keeping and unwrapping the `Just(y)` results and dropping `None`. Filter + map fused. **Examples** ```kex [1, 2, 3, 4].collect { |n| n > 2 then Just(n * 10) else None } # => [30, 40] ``` #### `take` ```kex take(n: Integer) -> [X] ``` Returns the first `n` elements. **Examples** ```kex [1, 2, 3, 4, 5].take(3) # => [1, 2, 3] ``` #### `drop` ```kex drop(n: Integer) -> [X] ``` Drops the first `n` elements. **Examples** ```kex [1, 2, 3, 4, 5].drop(2) # => [3, 4, 5] ``` #### `push` ```kex push(x: X) -> [X] ``` Returns a new list with `x` appended at the end. **Examples** ```kex [1, 2].push(3) # => [1, 2, 3] ``` #### `reverse` ```kex reverse : [X] ``` Returns the elements in reverse order. **Examples** ```kex [1, 2, 3].reverse # => [3, 2, 1] ``` #### `zip` ```kex zip(other: [Y]) -> [(X, Y)] ``` Pairs each element of this list with the corresponding element of `other`. Stops at the end of the shorter list. **Examples** ```kex [1, 2, 3].zip(["a", "b", "c"]) # => [(1, "a"), (2, "b"), (3, "c")] ``` #### `sort` ```kex sort : [X] ``` Returns the elements sorted in ascending natural order. **Examples** ```kex [3, 1, 2].sort # => [1, 2, 3] ``` ```kex sort(comp: (X -> X -> Bool)) -> [X] ``` Returns the elements sorted using a custom comparator. `comp` should return `true` when its first argument should come before its second. **Examples** ```kex [3, 1, 2].sort { |a, b| a > b } # => [3, 2, 1] ``` #### `min` ```kex min(f: (X -> Y)) -> X? ``` Returns the element with the smallest `f` key wrapped in `Just`, or `None` for an empty list. **Examples** ```kex ["hey", "hi"].min { |s| s.count } # => Just("hi") ``` #### `max` ```kex max(f: (X -> Y)) -> X? ``` Returns the element with the largest `f` key wrapped in `Just`, or `None` for an empty list. **Examples** ```kex ["hey", "hi"].max { |s| s.count } # => Just("hey") ``` #### `sum` ```kex sum(f: (X -> Number)) -> Number ``` Maps each element through `f` and sums the results. **Examples** ```kex ["hello", "hi"].sum { |s| s.count } # => 7 ``` #### `join` ```kex join(sep: String) -> String join : String ``` Renders and concatenates the elements, placing `sep` between adjacent values. With no separator, the rendered values are joined directly. Elements do not have to be strings: each is rendered using the same user-facing conversion used by interpolation and printing. **Parameters** - `sep` — text placed between adjacent elements **Returns**: the concatenated rendering **Examples** ```kex ["hello", "world", "kex"].join(", ") # => "hello, world, kex" ["a", "b", "c"].join # => "abc" [1, 2, 3].join(" + ") # => "1 + 2 + 3" ``` ### On `[Number]` #### `sum` ```kex sum : Number ``` Sums all elements. Returns `0` for an empty list. **Examples** ```kex [1, 2, 3].sum # => 6 [].sum # => 0 ``` #### `product` ```kex product : Number ``` Multiplies all elements. Returns `1` for an empty list. **Examples** ```kex [1, 2, 3].product # => 6 [].product # => 1 ``` ```kex product(f: (X -> Number)) -> Number ``` Maps each element through `f` and multiplies the results. **Examples** ```kex [[1,2],[3,4]].product { |pair| pair.first.or(1) } # => 3 ``` #### `min` ```kex min : X? ``` Returns the smallest element wrapped in `Just`, or `None` for an empty list. **Examples** ```kex [3, 1, 2].min # => Just(1) [].min # => None ``` #### `max` ```kex max : X? ``` Returns the largest element wrapped in `Just`, or `None` for an empty list. **Examples** ```kex [3, 1, 2].max # => Just(3) [].max # => None ``` ### On `[[Y]]` #### `flatten` ```kex flatten : [Y] ``` Flattens exactly one level of nesting. **Examples** ```kex [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4] ``` ### From [`Monoid`](algebra.md#trait-monoid) - [`repeat`](algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Enumerable`](enumerable.md#trait-enumerable) - [`mapIndexed`](enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. ### From [`Foldable`](enumerable.md#trait-foldable) - [`eachIndexed`](enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. ### Defined in other modules - [Algebra](algebra.md#make-list): [`identity`](algebra.md#list-identity), [`combine`](algebra.md#list-combine) - [Blankable](blankable.md#make-list): [`blank?`](blankable.md#list-blank?) - [Truthyable](truthyable.md#make-list): [`truthy?`](truthyable.md#list-truthy?) --- --- package: prelude version: "0.4.0-rc.3-dev" source: map.kex title: Map entities: - { kind: type, name: "Map" } - { kind: make, name: "Map" } --- # Map An immutable key-value store, written `{key: value}`. Keys are compared by structural equality and may be of any type; atom keys get the shorthand `{name: "Ada"}`, string keys are written out in full as `{"name": "Ada"}`. Updates return a new map: the `!` forms (`put!`, `delete!`) build a new map and rebind the receiver variable, they do not modify anything in place. Entries come back in canonical key order, not insertion order, so `keys`, `values`, `entries` and any traversal are stable and comparable across equal maps. ```kex let config = { host: "localhost", port: 8080 } config.get(:host).or("0.0.0.0") # => "localhost" config.get(:user).or("anonymous") # => "anonymous" config.put(:port, 9090) # => { :host: "localhost", :port: 9090 } ``` A map is `Enumerable` and `Foldable`, and the traversal blocks take the key and value as two parameters: ```kex config.each { |k, v| IO.printLine("${k} = ${v}") } config.filter { |k, v| k != :port } # => { :host: "localhost" } ``` ## type `Map` Declared for the same reason list.kex declares `type List = [X]`: it gives the name `Map` a source declaration, so it resolves as a type through the collected interfaces rather than needing to be known to the compiler. Implements [`Enumerable`](enumerable.md#trait-enumerable), [`Foldable`](enumerable.md#trait-foldable), [`Monoid`](algebra.md#trait-monoid), [`Blankable`](blankable.md#trait-blankable), [`Truthyable`](truthyable.md#trait-truthyable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: A, g: (A -> (K, V) -> A)) -> A ``` Folds over the map's `(key, value)` pairs in canonical key order. This is `Map`'s `Enumerable` primitive: `map`, `filter`, `find`, `any?` and the rest are built on it. The block receives the accumulator and one pair; destructure the pair to name its halves. **Parameters** - `acc` — the initial accumulator - `g` — combines the accumulator with each pair **Returns**: the final accumulator **Examples** _Summing the values_ ```kex { a: 1, b: 2 }.reduce(0) do |acc, pair| let (key, value) = pair acc + value end # => 3 ``` _Rendering the map as a query string_ ```kex { a: 1, b: 2 }.entries.map { |k, v| "${k}=${v}" }.join("&") ``` #### `identity` (from Monoid) The empty map: the identity element of the `Monoid` instance, so `m.combine({})` is `m`. **Returns**: `Map` — the empty map **Examples** ```kex Map.identity # => {} ``` #### `combine` (from Monoid) ```kex combine(other: This) -> This ``` Combines two maps by merging them, with `other`'s values winning on a key conflict. The `Monoid` operation, and the same thing `merge` does. **Parameters** - `other` — the map to merge in **Returns**: the combined map **Examples** ```kex { a: 1 }.combine({ b: 2 }) # => { :a: 1, :b: 2 } ``` _Folding a list of maps into one_ ```kex [{ a: 1 }, { b: 2 }, { a: 9 }].reduce({}) { |acc, m| acc.combine(m) } # => { :a: 9, :b: 2 } ``` #### `get` ```kex get(key: K) -> V? get(key: K, default: V) -> V ``` Returns the value stored under `key`, or `None` when the key is absent. Missing keys are an ordinary answer rather than a failure, so a lookup on data you did not produce is safe by default. Use the two-argument form below when you have a sensible fallback. **Parameters** - `key` — the key to look up **Returns**: the value, or `None` **Examples** ```kex let user = { name: "Alice", age: 32 } user.get(:name) # => Just("Alice") user.get(:missing) # => None ``` _Chaining through a nested map_ ```kex settings.get(:server).flatMap { |s| s.get(:port) }.or(8080) ``` #### `put` ```kex put(k: K, v: V) -> Map ``` Returns a new map with `key` mapped to `value`, replacing any previous entry for that key. The receiver is untouched. Use `put!` when you want the variable holding the map to be rebound to the result. **Parameters** - `k` — the key to set - `v` — the value to store **Returns**: a new map including the entry **Examples** ```kex {}.put(:x, 1) # => { :x: 1 } { x: 1 }.put(:x, 2) # => { :x: 2 } ``` _Rebinding with the +!+ form_ ```kex var totals = {} totals.put!(:visits, 1) totals # => { :visits: 1 } ``` #### `delete` ```kex delete(key: K) -> Map ``` Returns a new map without `key`. A key that is not present is not an error: the map comes back unchanged. Use `delete!` to rebind the receiver variable. **Parameters** - `key` — the key to remove **Returns**: a new map without that entry **Examples** ```kex { a: 1, b: 2 }.delete(:a) # => { :b: 2 } { a: 1 }.delete(:z) # => { :a: 1 } ``` _Stripping a secret before logging_ ```kex IO.printLine(params.delete(:password)) ``` #### `has?` ```kex has?(key: K) -> Bool ``` Returns `true` when `key` has an entry in the map. Distinguishes a missing key from one whose value is itself empty, which a `get` with a default cannot. **Parameters** - `key` — the key to test **Returns**: `true` when the key is present **Examples** ```kex { a: 1 }.has?(:a) # => true { a: 1 }.has?(:z) # => false ``` _Checking a required setting_ ```kex if !config.has?(:host) IO.printError("host is required") end ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the map has no entries. **Returns**: `true` for the empty map **Examples** ```kex {}.empty? # => true { a: 1 }.empty? # => false ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of entries. **Returns**: the entry count **Examples** ```kex { a: 1, b: 2 }.count # => 2 {}.count # => 0 ``` ```kex count(pred: (K -> V -> Bool)) -> Integer ``` Returns the number of entries satisfying `pred`. **Parameters** - `pred` — the test applied to each entry **Returns**: how many entries matched **Examples** ```kex { a: 1, b: 2 }.count { |k, v| v > 1 } # => 1 ``` _How many settings are still at their default_ ```kex config.count { |k, v| v == defaults.get(k, v) } ``` #### `keys` ```kex keys : [K] ``` Returns the map's keys as a list, in canonical key order. **Returns**: the keys **Examples** ```kex { b: 1, a: 2 }.keys # => [:a, :b] ``` _Reporting unrecognised options_ ```kex params.keys.reject { |k| known.contains?(k) } ``` #### `values` ```kex values : [V] ``` Returns the map's values as a list, ordered by their keys. **Returns**: the values **Examples** ```kex { a: 1, b: 2 }.values # => [1, 2] { a: 1, b: 2 }.values.sum # => 3 ``` #### `entries` ```kex entries : [(K, V)] ``` Returns the map's entries as a list of `(key, value)` tuples, in canonical key order. This is the bridge to the `List` functions a map does not have of its own, and the form the two-parameter blocks elsewhere are splatting from. **Returns**: the entries **Examples** ```kex { a: 1, b: 2 }.entries # => [(:a, 1), (:b, 2)] ``` _Sorting entries by value_ ```kex scores.entries.sort { |x, y| x.items.last.or(0) > y.items.last.or(0) } ``` #### `each` (from Foldable) ```kex each(f: (K -> V -> Void)) -> Void ``` Calls `f` with each key and value, for its side effects. **Parameters** - `f` — called once per entry **Examples** ```kex scores.each { |k, v| IO.printLine("${k}: ${v}") } ``` #### `map` (from Enumerable) ```kex map(f: (K -> V -> R)) -> [R] ``` Applies `f` to each key and value and collects the results into a LIST. Note the return type: `map` comes from `Enumerable`, whose contract is to produce a list, because `f` may return anything at all. Use `mapValues` or `mapKeys` when you want a map back. **Parameters** - `f` — applied to each entry **Returns**: the results, in canonical key order **Examples** ```kex { "a": 1, "b": 2 }.map { |k, v| "${k}=${v}" } # => ["a=1", "b=2"] ``` _Building a header block_ ```kex headers.map { |name, value| "${name}: ${value}" }.join("\n") ``` #### `mapValues` ```kex mapValues(f: (V -> W)) -> Map ``` Returns a new map with every value replaced by `f(value)`. The keys are left alone. **Parameters** - `f` — applied to each value **Returns**: a map with the same keys and transformed values **Examples** ```kex { a: 1, b: 2 }.mapValues { |v| v * 10 } # => { :a: 10, :b: 20 } ``` _Normalising values read as text_ ```kex raw.mapValues { |s| s.trim.lowerCase } ``` #### `mapKeys` ```kex mapKeys(f: (K -> J)) -> Map ``` Returns a new map with every key replaced by `f(key)`. The values are left alone. If `f` maps two keys onto the same result, one entry wins: the map cannot hold both. **Parameters** - `f` — applied to each key **Returns**: a map with transformed keys **Examples** ```kex { "a": 1, "b": 2 }.mapKeys { |k| k.upperCase } # => { A: 1, B: 2 } ``` _Making header lookups case-insensitive_ ```kex headers.mapKeys { |name| name.lowerCase } ``` #### `filter` (from Enumerable) ```kex filter(pred: (K -> V -> Bool)) -> Map ``` Returns a new map with only the entries for which `pred` answers `true`. Map overrides the map-returning HOFs (Enumerable's default returns a list). **Parameters** - `pred` — the test applied to each entry **Returns**: the matching entries **Examples** ```kex { a: 1, b: 2, c: 3 }.filter { |k, v| v > 1 } # => { :b: 2, :c: 3 } ``` _Keeping only the options that were actually set_ ```kex options.filter { |name, value| !value.blank? } ``` #### `reject` ```kex reject(pred: (K -> V -> Bool)) -> Map ``` Returns a new map with the entries for which `pred` answers `true` removed. The complement of `filter`. **Parameters** - `pred` — the test applied to each entry **Returns**: the entries that failed the predicate **Examples** ```kex { a: 1, b: 2, c: 3 }.reject { |k, v| v > 1 } # => { :a: 1 } ``` _Dropping internal keys before serialising_ ```kex record.reject { |name, _| name.startsWith?("_") } ``` #### `merge` ```kex merge(other: Map) -> Map ``` Returns a new map holding the entries of both. When a key appears in both, `other`'s value wins. The right-biased rule is what makes this the natural way to apply overrides on top of defaults. **Parameters** - `other` — the map whose values take precedence **Returns**: the combined map **Examples** ```kex { a: 1, b: 2 }.merge({ b: 99, c: 3 }) # => { :a: 1, :b: 99, :c: 3 } ``` _Layering user settings over defaults_ ```kex defaults.merge(userConfig) ``` #### `any?` (from Foldable) ```kex any?(pred: (K -> V -> Bool)) -> Bool ``` Returns `true` when at least one entry satisfies `pred`. Stops at the first match. **Parameters** - `pred` — the test applied to each entry **Returns**: `true` when any entry matches **Examples** ```kex { a: 1, b: 2 }.any? { |k, v| v > 1 } # => true { a: 1, b: 2 }.any? { |k, v| v > 9 } # => false ``` #### `all?` (from Foldable) ```kex all?(pred: (K -> V -> Bool)) -> Bool ``` Returns `true` when every entry satisfies `pred`. The empty map answers `true`. **Parameters** - `pred` — the test applied to each entry **Returns**: `true` when all entries match **Examples** ```kex { a: 1, b: 2 }.all? { |k, v| v > 0 } # => true { a: 1, b: 2 }.all? { |k, v| v > 1 } # => false ``` _Validating a form_ ```kex fields.all? { |name, value| !value.blank? } ``` #### `find` (from Foldable) ```kex find(pred: (K -> V -> Bool)) -> (K, V)? ``` Returns the first entry satisfying `pred` as a `(key, value)` tuple, or `None` when nothing matches. "First" means first in canonical key order. **Parameters** - `pred` — the test applied to each entry **Returns**: the matching entry, or `None` **Examples** ```kex { a: 1, b: 2 }.find { |k, v| v > 1 } # => Just((:b, 2)) { a: 1, b: 2 }.find { |k, v| v > 9 } # => None ``` _Locating a value without knowing its key_ ```kex users.find { |id, user| user.email == target } ``` #### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the map has no entries. The `Blankable` view of `empty?`, so a map can be tested by the same generic code that tests strings and lists. **Returns**: `true` for the empty map **Examples** ```kex {}.blank? # => true { a: 1 }.blank? # => false ``` ### From [`Enumerable`](enumerable.md#trait-enumerable) - [`mapIndexed`](enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`flatMap`](enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](enumerable.md#trait-foldable) - [`eachIndexed`](enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. ### From [`Monoid`](algebra.md#trait-monoid) - [`repeat`](algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### Defined in other modules - [Truthyable](truthyable.md#make-map): [`truthy?`](truthyable.md#map-truthy?) --- --- package: prelude version: "0.4.0-rc.3-dev" source: math.kex title: Math entities: - { kind: module, name: "Math" } --- # Math ## module `Math` Mathematical constants and functions. All trigonometric functions work in radians. Every function here accepts a `Number` (an `Integer` or a `Float`) and the transcendental ones answer with a `Float`. A Kex `Float` is always finite, so a domain error (`Math.sqrt(-1.0)`) or an overflow (`Math.exp(1000.0)`) raises rather than producing `NaN` or `Infinity`: the same rule the BEAM enforces, where those two values cannot exist at all. There is no non-finite float to test for afterwards. ```kex Math.sqrt(2.0) # => 1.4142135623730951 Math.hypot(3.0, 4.0) # => 5.0 Math.sin(Math.PI / 2.0) # => 1.0 ``` The everyday operations on a single number: `abs`, `floor`, `ceil`, `round`, `sqrt`: can also be called with `Integer` and `Float` receivers, which usually reads better in a chain: `x.abs` over `Math.abs(x)`. ### `PI` (constant) The ratio of a circle's circumference to its diameter. **Examples** ```kex Math.PI # => 3.141592653589793 ``` _Degrees to radians_ ```kex let radians(deg: Float) -> Float = deg * Math.PI / 180.0 ``` ### `E` (constant) The base of the natural logarithm. **Examples** ```kex Math.E # => 2.718281828459045 Math.log(Math.E) # => 1.0 ``` ### `sqrt` ```kex sqrt(x: Number) -> Float ``` Returns the square root of `x`. Raises for a negative `x`, which has no real root. **Parameters** - `x` — a non-negative number **Returns**: the square root **Examples** ```kex Math.sqrt(9.0) # => 3.0 Math.sqrt(2.0) # => 1.4142135623730951 Math.sqrt(-1.0) # raises: Math.sqrt: undefined result (NaN) ``` _Distance between two points_ ```kex Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ``` ### `cbrt` ```kex cbrt(x: Number) -> Float ``` Returns the cube root of `x`. Unlike `sqrt`, negative input is fine: a negative number has a real cube root. **Parameters** - `x` — any number **Returns**: the cube root **Examples** ```kex Math.cbrt(27.0) # => 3.0 Math.cbrt(-8.0) # => -2.0 ``` ### `sin` ```kex sin(x: Number) -> Float ``` Returns the sine of `x`, given in radians. **Parameters** - `x` — the angle in radians **Returns**: the sine **Examples** ```kex Math.sin(0.0) # => 0.0 Math.sin(Math.PI / 2.0) # => 1.0 ``` _A point on a circle_ ```kex let y = centerY + radius * Math.sin(angle) ``` ### `cos` ```kex cos(x: Number) -> Float ``` Returns the cosine of `x`, given in radians. **Parameters** - `x` — the angle in radians **Returns**: the cosine **Examples** ```kex Math.cos(0.0) # => 1.0 Math.cos(Math.PI) # => -1.0 ``` ### `tan` ```kex tan(x: Number) -> Float ``` Returns the tangent of `x`, given in radians. **Parameters** - `x` — the angle in radians **Returns**: the tangent **Examples** ```kex Math.tan(0.0) # => 0.0 Math.tan(Math.PI / 4.0) # => 1.0 ``` ### `asin` ```kex asin(x: Number) -> Float ``` Returns the arc sine of `x` in radians, in the range -π/2 to π/2. **Parameters** - `x` — a value in -1..1 **Returns**: the angle in radians **Examples** ```kex Math.asin(0.0) # => 0.0 Math.asin(1.0) # => 1.5707963267948966 ``` ### `acos` ```kex acos(x: Number) -> Float ``` Returns the arc cosine of `x` in radians, in the range 0 to π. **Parameters** - `x` — a value in -1..1 **Returns**: the angle in radians **Examples** ```kex Math.acos(1.0) # => 0.0 Math.acos(0.0) # => 1.5707963267948966 ``` ### `atan` ```kex atan(x: Number) -> Float ``` Returns the arc tangent of `x` in radians, in the range -π/2 to π/2. Use `atan2` when you have both coordinates of a vector: it can tell the quadrant apart, and this cannot. **Parameters** - `x` — any number **Returns**: the angle in radians **Examples** ```kex Math.atan(0.0) # => 0.0 Math.atan(1.0) # => 0.7853981633974483 ``` ### `atan2` ```kex atan2(y: Number, x: Number) -> Float ``` Returns the angle of the vector `(x, y)` in radians, from -π to π. Both signs are taken into account, so the result lands in the correct quadrant, which is why this, not `atan`, is what you want for converting a vector to an angle. Note the argument order: `y` first. **Parameters** - `y` — the vertical component - `x` — the horizontal component **Returns**: the angle in radians **Examples** ```kex Math.atan2(1.0, 1.0) # => 0.7853981633974483 (45°) Math.atan2(1.0, -1.0) # => 2.356194490192345 (135°) ``` _The bearing from one point to another, in degrees_ ```kex Math.atan2(y2 - y1, x2 - x1) * 180.0 / Math.PI ``` ### `sinh` ```kex sinh(x: Number) -> Float ``` Returns the hyperbolic sine of `x`. **Parameters** - `x` — any number **Returns**: the hyperbolic sine **Examples** ```kex Math.sinh(0.0) # => 0.0 Math.sinh(1.0) # => 1.1752011936438014 ``` ### `cosh` ```kex cosh(x: Number) -> Float ``` Returns the hyperbolic cosine of `x`. **Parameters** - `x` — any number **Returns**: the hyperbolic cosine **Examples** ```kex Math.cosh(0.0) # => 1.0 Math.cosh(1.0) # => 1.5430806348152437 ``` ### `tanh` ```kex tanh(x: Number) -> Float ``` Returns the hyperbolic tangent of `x`, always between -1 and 1. **Parameters** - `x` — any number **Returns**: the hyperbolic tangent **Examples** ```kex Math.tanh(0.0) # => 0.0 Math.tanh(1.0) # => 0.7615941559557649 ``` ### `log` ```kex log(x: Number) -> Float log(x: Number, base: Number) -> Float ``` Returns the natural logarithm of `x`: its logarithm to base `e`. With a second argument, returns the logarithm to that base instead. Raises for `x` of zero or less, which has no real logarithm. **Parameters** - `x` — a positive number - `base` — the logarithm base; omitted for base `e` **Returns**: the logarithm **Examples** ```kex Math.log(Math.E) # => 1.0 Math.log(8.0, 2.0) # => 3.0 ``` _How many digits a number has_ ```kex Math.log10(n.to(Float).or(1.0)).floor + 1 ``` ### `log2` ```kex log2(x: Number) -> Float ``` Returns the base-2 logarithm of `x`. The same as `Math.log(x, 2.0)`, and more direct. **Parameters** - `x` — a positive number **Returns**: the base-2 logarithm **Examples** ```kex Math.log2(8.0) # => 3.0 Math.log2(1024.0) # => 10.0 ``` _Bits needed to represent n distinct values_ ```kex Math.log2(n.to(Float).or(1.0)).ceil ``` ### `log10` ```kex log10(x: Number) -> Float ``` Returns the base-10 logarithm of `x`. **Parameters** - `x` — a positive number **Returns**: the base-10 logarithm **Examples** ```kex Math.log10(1000.0) # => 3.0 Math.log10(1.0) # => 0.0 ``` ### `exp` ```kex exp(x: Number) -> Float ``` Returns `e` raised to the power `x`: the inverse of `Math.log`. Raises on overflow, which for a double happens a little past `x` of 709. **Parameters** - `x` — the exponent **Returns**: e to the power x **Examples** ```kex Math.exp(0.0) # => 1.0 Math.exp(1.0) # => 2.718281828459045 ``` _Exponential decay_ ```kex let remaining = initial * Math.exp(-rate * elapsed) ``` ### `pow` ```kex pow(x: Number, y: Number) -> Float ``` Returns `x` raised to the power `y`. The result is always a `Float`, even for whole arguments, so round it when you need an integer back. **Parameters** - `x` — the base - `y` — the exponent **Returns**: x to the power y **Examples** ```kex Math.pow(2.0, 10.0) # => 1024.0 Math.pow(2.0, 0.5) # => 1.4142135623730951 Math.pow(2.0, 10.0).round # => 1024 ``` _Compound growth_ ```kex principal * Math.pow(1.0 + rate, years) ``` ### `abs` ```kex abs(x: Number) -> Number ``` Returns the magnitude of `x`, discarding its sign. The type is preserved: an `Integer` in gives an `Integer` out. You can also write `x.abs` using UFCS. **Parameters** - `x` — any number **Returns**: the absolute value **Examples** ```kex Math.abs(-5) # => 5 Math.abs(-2.5) # => 2.5 ``` ### `floor` ```kex floor(x: Number) -> Integer ``` Returns the largest integer that is not greater than `x`: rounding toward negative infinity. **Parameters** - `x` — any number **Returns**: the floor **Examples** ```kex Math.floor(3.7) # => 3 Math.floor(-3.2) # => -4 ``` ### `ceil` ```kex ceil(x: Number) -> Integer ``` Returns the smallest integer that is not less than `x`: rounding toward positive infinity. **Parameters** - `x` — any number **Returns**: the ceiling **Examples** ```kex Math.ceil(3.2) # => 4 Math.ceil(-3.7) # => -3 ``` ### `hypot` ```kex hypot(x: Number, y: Number) -> Float ``` Returns the Euclidean distance +sqrt(x*x + y*y)+, computed so that large values do not overflow on the way. **Parameters** - `x` — the first leg - `y` — the second leg **Returns**: the hypotenuse **Examples** ```kex Math.hypot(3.0, 4.0) # => 5.0 Math.hypot(5.0, 12.0) # => 13.0 ``` _The length of a vector_ ```kex Math.hypot(velocity.x, velocity.y) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: mock.kex title: Mock entities: - { kind: type, name: "Reader" } - { kind: type, name: "Lookup" } - { kind: type, name: "Tick" } - { kind: type, name: "Writer" } - { kind: module, name: "Mock" } --- # Mock Mock: deterministic stand-ins for the world outside the program: the filesystem, environment, and console. Networking mocks live under Mock.Net. These are STATEFUL: `Mock.FS.File(path, content)` writes into a store the real `FS.File` then reads back, so a test can write and read again, and `clear()` undoes it. That state is global and lives until cleared, which is what makes hook ordering and write/read round trips testable, and also what makes two tests able to interfere. Use `with FS.File = MyFake { ... } do ... end` for a replacement limited to one block. Use mocks when a test needs state shared across several calls, and clear that state between tests. Import with `using Mock`. Everything here is test-only: the mock operations and the stand-in records (`Mock.Files`, `Mock.Env`, `Mock.Clock`) alike are allowed in spec files and the REPL, and need `--allow-mocks` anywhere else. ## type `Reader` A stand-in's read hook: a path in, its content or `None` out. ## type `Lookup` An environment stand-in's lookup hook: a name in, its value or `None` out. ## type `Tick` A clock stand-in's reading hook: nothing in, the instant out. **Variants** - `Block(DateTime)` ## type `Writer` An environment stand-in's WRITE hook: the name and the value a program set. A `Mock.Env` is an immutable record, so a write cannot land in its `vars`, and it must not reach the real environment, or a test would leak a variable into the next one and into anything the suite starts. The hook is how a test observes the write instead: record it, assert on it, or ignore it, which is what `None` does. ## module `Mock` ## record `Files` A stand-in for the `FS.File` capability, for `with FS.File = ...`. Unlike the `Mock.*` functions below it holds no global state, needs no `clear()`, and cannot leak past its block. What it cannot do is change: `write` then `read` back is not something a value does, so a test needing that round trip uses `Mock.FS`. ```kex with FS.File = Mock.Files { files: {"kex.toml": "name = \"demo\""} } do assert(loadConfig() == "demo") end ``` `onRead` takes over when a test needs an ANSWER rather than a fixture: content derived from the path, a failure on the third call, a record of what was asked for. It is consulted first and its `None` means "no such file", so a callback can model absence too. **Fields** - `files` : [Map](map.md#type-map)<[FS.FilePath](fs.md#type-fs-filepath), [String](string.md#make-string)> (optional) - `onRead` : [Reader](#type-reader)? (optional) Implements `FS.File`. ### Functions #### `cannedRead` ```kex cannedRead(path) ``` Named apart from `read`: `this.read(path)` would bind to the capability's own `read : FilePath -> String?`, not to this function. #### `read` ```kex read(path) ``` #### `readBytes` ```kex readBytes(path) ``` #### `readLines` ```kex readLines(path) ``` #### `feed` ```kex feed(path) ``` #### `size` ```kex size(path) ``` #### `exists?` ```kex exists?(path) ``` #### `file?` ```kex file?(path) ``` #### `directory?` ```kex directory?(path) ``` #### `absolute` ```kex absolute(path) ``` #### `canonical` ```kex canonical(path) ``` #### `symlink?` ```kex symlink?(path) ``` #### `info` ```kex info(path) ``` #### `open` ```kex open(path, mode) ``` A fake is a value, so there is nowhere for a write to go. Refusing is the honest answer and the useful one: a test that did not expect a write sees it fail rather than silently succeed. #### `write` ```kex write(path, content) ``` #### `writeBytes` ```kex writeBytes(path, content) ``` #### `append` ```kex append(path, content) ``` #### `delete` ```kex delete(path) ``` #### `copy` ```kex copy(src, dst) ``` #### `rename` ```kex rename(src, dst) ``` #### `changePermission` ```kex changePermission(path, permissions) ``` #### `changeMode` ```kex changeMode(path, mode) ``` ## record `Env` A stand-in for the `ENV` capability. A name simply left out of `vars` reads as unset, which is the whole reason `Mock.ENV.unset` exists: absence is an answer programs act on. `onGet` answers instead of the map when a test wants a rule rather than a fixture. ```kex with ENV = Mock.Env { vars: {"HOME": "/fake"} } do assert(configHome() == "/fake/.config") end ``` **Fields** - `vars` : [Map](map.md#type-map)<[String](string.md#make-string), [String](string.md#make-string)> (optional) - `onGet` : [Lookup](#type-lookup)? (optional) - `onSet` : [Writer](#type-writer)? (optional) Implements `ENV`. ### Functions #### `lookup` ```kex lookup(key) ``` Named apart from `get`: `this.get(key)` would bind to the capability's own `get`, not to this function. #### `get` ```kex get(key, default) ``` #### `has?` ```kex has?(key) ``` #### `keys` ```kex keys ``` #### `values` ```kex values ``` #### `count` ```kex count ``` #### `each` ```kex each(f) ``` #### `entries` ```kex entries ``` #### `set` ```kex set(name, value) ``` A write goes to `onSet` or nowhere. It must NOT reach the real environment: a substituted ENV is the whole point of the mock, and a test that set a variable would otherwise leak it into the next one and into every process the suite starts. `vars` cannot take it either: a record is immutable, so a test that cares about writes supplies the hook, and one that does not gets a write that goes quietly nowhere. #### `unset` ```kex unset(name) ``` ## record `Clock` A stand-in for the `Time.Clock` capability. Every reading in the region (`Time.now`, `Date.today`, `DateTime.utcNow`) answers from it. ```kex with Time.Clock = Mock.Clock { at: DateTime.parse("2026-07-30T14:03:00Z").try } do assert(Date.today().iso == "2026-07-30") end ``` The clock is frozen: every reading is `at`, zone included, so `Date.today()` is the date in the zone you wrote and not in the zone of the machine running the test. `onNow` answers instead when a test wants a rule rather than a fixture. A block keeps the clock of the region it was written in, so a rule written outside the `with` reads the real one. That is how to get a clock that still runs, which code measuring elapsed time needs: a frozen clock makes every interval zero. ```kex with Time.Clock = Mock.Clock { onNow: Just({ DateTime.now() + 3.days }) } do let started = DateTime.epochNanos() runTheThing() assert(DateTime.epochNanos() > started) end ``` **Fields** - `at` : [DateTime](time.md#record-datetime) (optional) - `onNow` : [Tick](#type-tick)? (optional) Implements `Time.Clock`. ### Functions #### `now` ```kex now ``` ## module `Mock.FS` A stateful stand-in for the filesystem: files a test declares, that the real `FS.File` then reads back. The store is global and lives until `clear`, which is what makes a write followed by a read testable, and what makes two tests able to interfere. Clear it in an `after` hook. ```kex describe "the config loader" do before do Mock.FS.files({ "app.conf": "port = 8080\n" }) end after do Mock.FS.clear() end it "reads the port" do Assert.equal(loadPort(), 8080) end end ``` ### `File` ```kex File(path: FS.FilePath, content: String) -> Void ``` Declares one file and its content. **Parameters** - `path` — the path the file appears at - `content` — its contents **Examples** ```kex Mock.FS.File("app.conf", "port = 8080\n") ``` ### `Directory` ```kex Directory(path: FS.FilePath) -> Void ``` Declares a directory at `path`. **Parameters** - `path` — the directory to declare **Examples** ```kex Mock.FS.Directory("src") ``` ### `clear` ```kex clear : Void ``` Empties the store, so nothing declared so far is visible any more. Call it in an `after` hook: the store is global, and what one test leaves behind the next one sees. **Examples** ```kex after do Mock.FS.clear() end ``` ### `files` ```kex files(entries: Map) -> Void ``` Declares the whole fixture in one call. The same shape `Mock.Files { files: ... }` takes: one line instead of one per file. **Parameters** - `entries` — paths to their contents **Examples** ```kex Mock.FS.files({ "app.conf": "port = 8080\n", "hosts": "localhost\n" }) ``` ### `onRead` ```kex onRead(reader: Reader) -> Void ``` Answers reads by RULE rather than from a fixture. Content derived from the path, a failure on the third call, a record of what was asked for. Consulted before the map, and returning `None` means "no such file", so absence is expressible too. **Parameters** - `reader` — the rule to answer reads with **Examples** _Every .txt path has content, nothing else exists_ ```kex Mock.FS.onRead do |path| path.endsWith?(".txt") then Just("stub") else None end ``` ## module `Mock.ENV` Overlays the process environment, so a test can say what `ENV` holds instead of depending on how it was launched. The overlay is global and lives until `clear`: clear it in an `after` hook. ```kex Mock.ENV.vars({ "HOME": "/fake", "LOG_LEVEL": "debug" }) assert(configPath() == "/fake/.config") Mock.ENV.clear() ``` ### `set` ```kex set(name: String, value: String) -> Void ``` Sets one variable in the overlay. **Parameters** - `name` — the variable name - `value` — its value **Examples** ```kex Mock.ENV.set("LOG_LEVEL", "debug") ``` ### `unset` ```kex unset(name: String) -> Void ``` Removes one variable from the overlay, so it reads as unset. Separate from `set` because a variable being ABSENT is an answer programs act on, and there is no value that means it. **Parameters** - `name` — the variable to remove **Examples** _Testing the unset path_ ```kex Mock.ENV.unset("HOME") Assert.equal(configPath(), ".") ``` ### `clear` ```kex clear : Void ``` Removes the whole overlay, restoring the real environment. **Examples** ```kex after do Mock.ENV.clear() end ``` ### `vars` ```kex vars(entries: Map) -> Void ``` Declares the whole overlay in one call. Supply a map of variable names to their values. Use `with ENV = Mock.Env { onGet: ... }` when a callback should decide the value for each lookup instead. **Parameters** - `entries` — variable names to their values **Examples** ```kex Mock.ENV.vars({ "HOME": "/fake", "LOG_LEVEL": "debug" }) ``` ## module `Mock.IO` A stateful stand-in for the console: captures what a program prints, and feeds it lines as if they had been typed. The way to test a program that talks to a person without one being there. ```kex Mock.IO.start() Mock.IO.input("Ada", "42") greet() Assert.equal(Mock.IO.output(), "hello, Ada\n") Mock.IO.stop() ``` ### `start` ```kex start : Void ``` Starts capturing output and serving queued input. **Examples** ```kex before do Mock.IO.start() end ``` ### `stop` ```kex stop : Void ``` Stops capturing, and restores the real console. **Examples** ```kex after do Mock.IO.stop() end ``` ### `output` ```kex output : String ``` Everything the program has printed since capturing started. Newlines are included, so a single `IO.printLine("hi")` gives `"hi\n"`. **Returns**: the captured output **Examples** ```kex Mock.IO.start() IO.printLine("hello") Assert.equal(Mock.IO.output(), "hello\n") ``` ### `clear` ```kex clear : Void ``` Discards the captured output, while continuing to capture. Useful between phases of one test, when only the later output matters. **Examples** ```kex setUpNoisily() Mock.IO.clear() theThingUnderTest() Assert.equal(Mock.IO.output(), "done\n") ``` ### `input` ```kex input(first, second, third, fourth) -> Void ``` Queues the lines `IO.getLine` will return, in order. Takes a list, or up to four lines as separate arguments. Once they run out, `IO.getLine` answers `None`: end of input, exactly as a closed stdin would. **Parameters** - `lines` — the lines to serve, in order **Examples** _A list of lines_ ```kex Mock.IO.input(["Ada", "42"]) ``` _The same, as separate arguments_ ```kex Mock.IO.input("Ada", "42") ``` ## module `Mock.Net` Scriptable networking values for testing HTTP, DNS, sockets and WebSockets. ## module `Mock.Net.HTTP` Canned HTTP transport state for networking specifications. ## record `Transport` Responses returned in order by a scripted transport. **Fields** - `responses` : [Any] ## module `Mock.Net.DNS` Canned DNS resolver state for networking specifications. ## record `ResolverScript` Hostname-to-address answers supplied without touching the network. **Fields** - `answers` : {[String](string.md#make-string): [[String](string.md#make-string)]} ## module `Mock.Net.Socket` Canned byte-stream state for socket specifications. ## record `Script` Binary chunks delivered in order as incoming socket data. **Fields** - `incoming` : [[Binary](binary.md#type-binary)] ## module `Mock.Net.WebSocket` Canned message state for WebSocket specifications. ## record `Script` High-level messages delivered in order by a scripted connection. **Fields** - `incoming` : [Any] --- --- package: prelude version: "0.4.0-rc.3-dev" source: net.kex title: Net entities: - { kind: module, name: "Net" } --- # Net ## module `Net` Shared networking values, capability discovery, and typed failures. ```kex using Net let https = Port.from(443).try if Support.current.tls.usable? then https.string else "TLS unavailable" end ``` ## record `Port` A validated TCP or UDP port number in `0..65535`. Use `Port.from` at input boundaries. Port zero requests an ephemeral port where a listening API permits it; after binding, ask the server or socket for the concrete port the operating system chose. **Fields** - `value` : [Integer](number.md#make-integer) ### Functions #### `string` ```kex string : String ``` Renders the decimal port without a host or scheme. **Returns**: decimal port text **Examples** _Building an address for display_ ```kex IO.printLine("listening on 127.0.0.1:${port.string}") ``` ## record `SupportValue` Whether a feature was compiled into this backend and is usable now. `compiled?` describes the build; `usable?` also accounts for the environment it is running in. A browser build may contain an HTTP client, for example, while browser policy still prevents a particular lower-level capability. **Fields** - `compiled?` : [Bool](truthyable.md#make-bool) - `usable?` : [Bool](truthyable.md#make-bool) ## record `SupportReport` Granular networking capabilities for the current backend and environment. Read this before choosing a transport dynamically. Applications that require one capability can instead check that single field during startup and fail with a useful message. **Fields** - `dns` : [SupportValue](#record-net-supportvalue) - `tcp` : [SupportValue](#record-net-supportvalue) - `udp` : [SupportValue](#record-net-supportvalue) - `unix` : [SupportValue](#record-net-supportvalue) - `tls` : [SupportValue](#record-net-supportvalue) - `httpClient` : [SupportValue](#record-net-supportvalue) - `httpServer` : [SupportValue](#record-net-supportvalue) - `webSocketClient` : [SupportValue](#record-net-supportvalue) - `webSocketServer` : [SupportValue](#record-net-supportvalue) ## module `Net.Port` Validated `Port` construction. ### `from` ```kex from(value: Integer) -> Result ``` Validates a port number. **Parameters** - `value` — a number in `0..65535` **Returns**: the validated port, or `Parse` **Examples** _Reading a listen port from the environment_ ```kex let number = ENV.get("PORT").flatMap { |text| text.to(Integer) }.or(8080) let port = Port.from(number).try ``` ## module `Net.Support` Runtime discovery for optional network transports and protocols. ### `current` (constant) ```kex current : SupportReport ``` Reports compiled and currently usable networking features. **Examples** _Explaining why an HTTP-dependent command cannot run_ ```kex if !Support.current.httpClient.usable? die("this Kex build cannot make HTTP requests here") end ``` ## type `NetOperation` The subsystem or operation that produced a networking error. **Variants** - `DNS` - `TCP` - `UDP` - `Unix` - `TLS` - `HTTPClient` - `HTTPServer` - `WebSocketClient` - `WebSocketServer` ## type `NetErrorKind` Stable, backend-independent networking failure categories. **Variants** - `Parse` - `Resolve` - `Connect` - `Protocol` - `Timeout` - `Cancelled` - `Limit` - `Closed` - `ReadInProgress` - `UnsupportedBackend` - `UnsupportedOption` - `BrowserRestricted` - `OpaqueRedirect` - `MockEmpty` - `Backend` ## record `NetError` A typed networking failure shared by every network module. `kind` is the stable category to branch on. `message` is for a person, while `phase` adds protocol context such as a TLS handshake and `progress` records bytes transferred before a partial-operation failure. Keeping those roles separate lets programs recover without matching backend-specific prose. **Fields** - `kind` : [NetErrorKind](#type-net-neterrorkind) - `operation` : [NetOperation](#type-net-netoperation) - `message` : [String](string.md#make-string) - `phase` : [String](string.md#make-string)? - `progress` : [Integer](number.md#make-integer)? --- --- package: prelude version: "0.4.0-rc.3-dev" source: node.kex title: Node entities: - { kind: module, name: "Node" } --- # Node ## module `Node` Distributed Kex: this BEAM node, and the other nodes it is connected to. A node is one running BEAM VM with a name. Once two nodes share a cookie and are connected, a `Pid` from one works on the other: `send`, `link` and `monitor` cross the network unchanged, and so do typed `Process` and `Server` handles. Name a node when starting it (`kex --sname a --cookie secret app.kex`), or from inside the program with `Node.start`: ```kex using Node main do Node.connect(:b@myhost) Node.send(:b@myhost, :logger, (:hello, Node.self)) end ``` Node names are atoms: `:b@myhost` for a short name, the quoted `:"b@host.example.com"` for one with dots, or `Atom.from(text)` for one built at runtime. Both nodes need the same compiled code for anything that carries a function, like a lambda or a record used as an argument to functions on the remote node; `Node.spawn` sends its block's code along. Plain data — numbers, strings, lists, tuples, records — needs nothing. Backed by Kex.Intrinsic.Node on the BEAM. The tree-walk interpreter is always a single, unnamed node. ### `self` ```kex self : Atom ``` This node's name, or `:nonode@nohost` when it is not distributed. **Returns**: the node name **Examples** ```kex Node.self # => :a@myhost ``` ### `alive?` ```kex alive? : Bool ``` Whether this node is distributed — started with a name, so other nodes can connect to it. **Returns**: true once the node has a name **Examples** ```kex Node.alive? # => false, unless started with --sname or Node.start ``` ### `list` ```kex list : [Atom] ``` The nodes this one is currently connected to, not including itself. **Returns**: the connected node names **Examples** ```kex Node.list # => [:b@myhost] ``` ### `start` ```kex start(name: Atom) -> Result ``` Makes this node distributed under `name`, as `--sname`/`--name` would at startup. A `name` whose host has a dot (`:"app@host.example.com"`) uses long names; anything else uses short names. **Parameters** - `name` — the node name, like `:app` or `:app@myhost` **Returns**: the full node name, or why it could not start **Examples** ```kex Node.start(:app) # => Ok(:app@myhost) ``` ### `stop` ```kex stop : Bool ``` Stops distribution: the node drops its name and every connection. **Returns**: whether it was distributed and has now stopped ### `connect` ```kex connect(node: Atom) -> Bool ``` Connects to the node named `node`. Both nodes must share a cookie. **Parameters** - `node` — the node to connect to **Returns**: whether the connection is up **Examples** ```kex Node.connect(:b@myhost) # => true ``` ### `disconnect` ```kex disconnect(node: Atom) -> Bool ``` Drops the connection to `node`. **Parameters** - `node` — the node to disconnect from **Returns**: whether a connection was dropped ### `setCookie` ```kex setCookie(cookie: String) -> Void ``` Sets the cookie this node presents when connecting. Nodes connect only when their cookies match. **Parameters** - `cookie` — the shared secret ### `send` ```kex send : Atom -> Atom -> X -> Void ``` Sends `message` to the process registered as `name` on `node`, unchanged. Like `Pid.send`, it never blocks and never fails. **Parameters** - `node` — the node the process runs on - `name` — the name it was registered under - `message` — the message **Examples** ```kex Node.send(:b@myhost, :logger, (:info, "started")) ``` ### `whereIs` ```kex whereIs(node: Atom, name: Atom) -> Pid? ``` The `Pid` registered as `name` on `node`, or `None`. **Parameters** - `node` — the node to ask - `name` — the registered name **Returns**: the process, or `None` **Examples** ```kex Node.whereIs(:b@myhost, :logger).map { |pid| pid.send(:flush) } ``` ### `spawn` ```kex spawn(node: Atom, block: Block) -> Pid ``` Runs `block` in a new process on `node` and returns its `Pid`. The block is a function of THIS program. When `node` has not loaded the module it belongs to, that module's compiled code is sent over and loaded first. A module `node` already has is left as it is. **Parameters** - `node` — where to run it - `block` — the work **Returns**: the new process **Examples** ```kex let me = Process.self Node.spawn(:b@myhost) do me.send(Node.self) end ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: number.kex title: NumberFormat entities: - { kind: make, name: "Integer" } - { kind: make, name: "Float" } - { kind: module, name: "Integer" } - { kind: module, name: "Float" } - { kind: module, name: "Float32" } - { kind: module, name: "Float64" } - { kind: module, name: "Int8" } - { kind: module, name: "Int16" } - { kind: module, name: "Int32" } - { kind: module, name: "Int64" } - { kind: module, name: "UInt8" } - { kind: module, name: "UInt16" } - { kind: module, name: "UInt32" } - { kind: module, name: "UInt64" } - { kind: module, name: "Byte" } - { kind: module, name: "Number" } - { kind: record, name: "NumberFormat" } - { kind: module, name: "NumberFormat" } - { kind: make, name: "Number" } --- # NumberFormat ## type `Integer` Implements [`Monoid`](algebra.md#trait-monoid), [`Group`](algebra.md#trait-group), [`Blankable`](blankable.md#trait-blankable), [`Truthyable`](truthyable.md#trait-truthyable). Whole numbers, of arbitrary size. `Integer` has no width limit: factorials and cryptographic moduli are ordinary values, not a special big-number type you have to opt into. `Integer` and `Float` are one numeric tower: they compare and order across the boundary, so `0 == 0.0` is `true` and `[1, 2.5, 3].sort` works. What differs is the arithmetic. `/` on two integers is integer division, and `sqrt` answers a `Float` because a square root generally is one. ```kex 7 / 2 # => 3 (integer division) 7.0 / 2.0 # => 3.5 16.sqrt # => 4.0 (a Float, even from an Integer) (-7).modulo(3) # => 2 (mathematical modulo, not C remainder) ``` ### `even?` ```kex even? : Bool ``` Returns `true` when the integer is divisible by two. Zero is even, and negative numbers follow the same rule. **Returns**: `true` for an even integer **Examples** ```kex 4.even? # => true 3.even? # => false (-2).even? # => true ``` _Splitting a list into evens and odds_ ```kex [1, 2, 3, 4].partition { |n| n.even? } # => ([2, 4], [1, 3]) ``` ### `odd?` ```kex odd? : Bool ``` Returns `true` when the integer is not divisible by two. The opposite of `even?`. **Returns**: `true` for an odd integer **Examples** ```kex 3.odd? # => true 4.odd? # => false ``` ### `abs` ```kex abs : Integer ``` Returns the magnitude of the integer, discarding its sign. **Returns**: the absolute value **Examples** ```kex (-5).abs # => 5 5.abs # => 5 ``` _Distance between two positions_ ```kex (a - b).abs ``` ### `sqrt` ```kex sqrt : Float ``` Returns the square root as a `Float`. The result is a `Float` even when the root is exact, because in general it is not. Round or floor it when you need an integer back. **Returns**: the square root **Examples** ```kex 16.sqrt # => 4.0 2.sqrt # => 1.4142135623730951 16.sqrt.round # => 4 ``` ### `modulo` ```kex modulo(n: Integer) -> Integer ``` Returns `this` modulo `n`. The result takes the sign of `n`, which is mathematical modulo rather than the C-style remainder other languages give. That is what makes it safe for wrapping an index that may have gone negative. **Parameters** - `n` — the modulus **Returns**: the remainder, with the sign of `n` **Examples** ```kex 7.modulo(3) # => 1 (-7).modulo(3) # => 2 ``` _Wrapping an index around a list_ ```kex let items = ["a", "b", "c"] items.at((-1).modulo(items.count)) # => Just("c") ``` ### `in?` ```kex in?(range: Range) -> Bool ``` Returns `true` when the integer falls inside `range`, endpoints included. **Parameters** - `range` — the inclusive range **Returns**: `true` when the integer is in range **Examples** ```kex 5.in?(1..10) # => true 11.in?(1..10) # => false ``` _Validating a port number_ ```kex port.in?(1..65535) ``` ### `times` ```kex times(block: (Integer -> Void)) -> Void times(block: Block) -> Void ``` Calls `f` exactly `this` times, passing the 0-based iteration index. This is the counting loop. When you want the numbers themselves rather than a count of repetitions, `(1..n).items.each` often reads better. **Parameters** - `f` — called once per iteration with the index **Examples** ```kex 3.times { |i| IO.printLine(i) } # prints 0, then 1, then 2 ``` _Repeating an action_ ```kex retries.times { |_| attemptConnection } ``` _Repeating an action that needs no index_ ```kex 3.times do IO.printLine("hi") end ``` ### `floor` ```kex floor : Integer ``` Returns the integer unchanged. Present so that code written against `Number` works whichever half of the tower it is handed. **Returns**: the same integer **Examples** ```kex 7.floor # => 7 ``` ### `ceil` ```kex ceil : Integer ``` Returns the integer unchanged. The `Integer` counterpart of `Float.ceil`. **Returns**: the same integer **Examples** ```kex 7.ceil # => 7 ``` ### `round` ```kex round : Integer ``` Returns the integer unchanged. The `Integer` counterpart of `Float.round`. **Returns**: the same integer **Examples** ```kex 7.round # => 7 ``` ### From [`Monoid`](algebra.md#trait-monoid) - [`repeat`](algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### Defined in other modules - [Algebra](algebra.md#make-integer): [`identity`](algebra.md#integer-identity), [`combine`](algebra.md#integer-combine), [`inverse`](algebra.md#integer-inverse) - [Blankable](blankable.md#make-integer): [`blank?`](blankable.md#integer-blank?) - [Time](time.md#make-integer): [`nanoseconds`](time.md#integer-nanoseconds), [`microseconds`](time.md#integer-microseconds), [`milliseconds`](time.md#integer-milliseconds), [`seconds`](time.md#integer-seconds), [`minutes`](time.md#integer-minutes), [`hours`](time.md#integer-hours), [`days`](time.md#integer-days), [`weeks`](time.md#integer-weeks), [`months`](time.md#integer-months), [`years`](time.md#integer-years) - [Truthyable](truthyable.md#make-integer): [`truthy?`](truthyable.md#integer-truthy?) - [Units](units.md#make-integer): [`nanosecond`](units.md#integer-nanosecond), [`microsecond`](units.md#integer-microsecond), [`millisecond`](units.md#integer-millisecond), [`second`](units.md#integer-second), [`minute`](units.md#integer-minute), [`hour`](units.md#integer-hour), [`day`](units.md#integer-day), [`week`](units.md#integer-week), [`timeMeasure`](units.md#integer-timemeasure) - [Units.Data](units/data.md#make-integer): [`<`](units/data.md#integer-op-lt), [`>`](units/data.md#integer-op-gt), [`<=`](units/data.md#integer-op-lt-eq), [`>=`](units/data.md#integer-op-gt-eq) ## type `Float` Implements [`Blankable`](blankable.md#trait-blankable), [`Truthyable`](truthyable.md#trait-truthyable). Double-precision floating-point numbers. A Kex `Float` is always finite. An operation that would produce `NaN` or `Infinity` raises instead: the same rule the BEAM enforces, where those two values cannot exist at all. So a `Float` you are holding is always a real number, and there is no `nan?` to check for. `Float` and `Integer` compare and order across the boundary; see `Integer` for the rest of the numeric tower. ```kex 3.7.floor # => 3 3.7.round # => 4 (-3.7).toInteger # => -3 (truncates toward zero) ``` ### `abs` ```kex abs : Float ``` Returns the magnitude of the float, discarding its sign. **Returns**: the absolute value **Examples** ```kex (-3.14).abs # => 3.14 3.14.abs # => 3.14 ``` _An approximate-equality test_ ```kex (a - b).abs < 0.0001 ``` ### `sqrt` ```kex sqrt : Float ``` Returns the square root. Raises for a negative receiver, which has no real root. **Returns**: the square root **Examples** ```kex 25.0.sqrt # => 5.0 2.0.sqrt # => 1.4142135623730951 ``` ### `in?` ```kex in?(range: Range) -> Bool ``` Returns `true` when the float falls inside `range`, endpoints included. **Parameters** - `range` — the inclusive range **Returns**: `true` when the float is in range **Examples** _Comparing directly is usually clearer for floats_ ```kex let ratio = 0.75 ratio >= 0.0 && ratio <= 1.0 # => true ``` ### `floor` ```kex floor : Integer ``` Returns the largest integer that is not greater than the float: it always rounds toward negative infinity, which is what separates it from `toInteger` for negative values. **Returns**: the floor **Examples** ```kex 3.7.floor # => 3 3.2.floor # => 3 (-3.2).floor # => -4 ``` ### `ceil` ```kex ceil : Integer ``` Returns the smallest integer that is not less than the float: it always rounds toward positive infinity. **Returns**: the ceiling **Examples** ```kex 3.2.ceil # => 4 3.0.ceil # => 3 (-3.7).ceil # => -3 ``` _How many pages of a fixed size a list needs_ ```kex (items.count.to(Float).or(0.0) / 20.0).ceil ``` ### `round` ```kex round : Integer ``` Returns the nearest integer, with halves rounding away from zero. **Returns**: the rounded value **Examples** ```kex 3.2.round # => 3 3.5.round # => 4 3.7.round # => 4 ``` ### `toInteger` ```kex toInteger : Integer ``` Truncates toward zero and returns the result as an `Integer`: it drops the fractional part rather than rounding, so it differs from both `floor` and `round` for negative values. **Returns**: the truncated value **Examples** ```kex 3.7.toInteger # => 3 (-3.7).toInteger # => -3 (floor would give -4) ``` ### Defined in other modules - [Blankable](blankable.md#make-float): [`blank?`](blankable.md#float-blank?) - [Time](time.md#make-float): [`nanoseconds`](time.md#float-nanoseconds), [`microseconds`](time.md#float-microseconds), [`milliseconds`](time.md#float-milliseconds), [`seconds`](time.md#float-seconds), [`minutes`](time.md#float-minutes), [`hours`](time.md#float-hours), [`days`](time.md#float-days), [`weeks`](time.md#float-weeks) - [Truthyable](truthyable.md#make-float): [`truthy?`](truthyable.md#float-truthy?) - [Units](units.md#make-float): [`nanosecond`](units.md#float-nanosecond), [`microsecond`](units.md#float-microsecond), [`millisecond`](units.md#float-millisecond), [`second`](units.md#float-second), [`minute`](units.md#float-minute), [`hour`](units.md#float-hour), [`day`](units.md#float-day), [`week`](units.md#float-week), [`timeMeasure`](units.md#float-timemeasure) ## module `Integer` Reading integers out of text. Use `parse` when a failure needs explaining and `"42".to(Integer)` when it does not: `to` answers a plain `Optional`, `parse` answers a `Result` carrying a `ParseError` that says where it stopped and what it had read so far. ### `parse` ```kex parse(s: String) -> Result parse(s: String, radix: Integer) -> Result ``` Parses the whole string as a base-10 integer. The string must be entirely consumed: leading or trailing characters make it an `Error`, with a `ParseError` describing where parsing stopped. Use `parsePrefix` when a trailing remainder is expected. **Parameters** - `s` — the text to parse **Returns**: the integer, or a described failure **Examples** ```kex Integer.parse("42") # => Ok(42) Integer.parse("-7") # => Ok(-7) Integer.parse("4x") # => Error(ParseError { ... rest: "x" ... }) ``` _Reading a setting with a fallback_ ```kex let port = Integer.parse(raw).or(8080) ``` _Reporting why the input was rejected_ ```kex match Integer.parse(raw) do Ok(n) => IO.printLine("port ${n}") Error(e) => IO.printError("bad port: ${e.message}") end ``` ### `parsePrefix` ```kex parsePrefix(s: String) -> (Integer, String)? ``` Parses an integer from the front of the string and returns it together with whatever text was left over. This is the building block for hand-written scanners: parse a number, keep going from the remainder. Answers `None` when the string does not begin with a number at all. **Parameters** - `s` — the text to parse from **Returns**: the value and the unconsumed remainder **Examples** ```kex Integer.parsePrefix("42abc") # => Just((42, "abc")) Integer.parsePrefix("abc") # => None ``` _Reading a leading count off a line_ ```kex match Integer.parsePrefix("3 items") do Just(pair) => do let (count, rest) = pair IO.printLine("${count} of${rest}") # prints: 3 of items end None => IO.printError("no leading count") end ``` ## module `Float` Reading floating-point numbers out of text. ### `parse` ```kex parse(s: String) -> Result ``` Parses the whole string as a float. The string must be entirely consumed; anything left over makes it an `Error` carrying a `ParseError`. **Parameters** - `s` — the text to parse **Returns**: the float, or a described failure **Examples** ```kex Float.parse("3.5") # => Ok(3.5) Float.parse("-0.25") # => Ok(-0.25) Float.parse("3.5m") # => Error(ParseError { ... rest: "m" ... }) ``` _Averaging a column of text values_ ```kex rows.map { |r| Float.parse(r).or(0.0) }.sum / rows.count.to(Float).or(1.0) ``` ### `parsePrefix` ```kex parsePrefix(s: String) -> (Float, String)? ``` Parses a float from the front of the string and returns it together with the unconsumed remainder. Answers `None` when the string does not begin with a number. **Parameters** - `s` — the text to parse from **Returns**: the value and the remainder **Examples** ```kex Float.parsePrefix("3.5rest") # => Just((3.5, "rest")) Float.parsePrefix("rest") # => None ``` _Splitting a measurement from its unit_ ```kex Float.parsePrefix("12.5kg") # => Just((12.5, "kg")) ``` ### `MAX` (constant) ```kex MAX : Float ``` The largest finite `Float`. A Kex `Float` is always finite, so there is no infinity to start from: this is the bound to use instead, say as the first "smallest so far". **Examples** ```kex let smallest = readings.reduce(Float.MAX) { |low, x| x < low then x else low } ``` ### `MIN` (constant) ```kex MIN : Float ``` The most negative finite `Float`: `-Float.MAX`. Not the smallest positive one, which some languages call MIN. ## module `Float32` The bounds of the sized numeric types: `Int8.MAX`, `UInt64.MAX`, `Float32.MIN` and so on. `Integer` has none: it is arbitrary-precision. For a float type, `MIN` is the most negative finite value, not the smallest positive one. The range of a 32-bit float. ### `MAX` (constant) ```kex MAX : Float32 ``` The largest Float32: 3.4028234663852886e38. ### `MIN` (constant) ```kex MIN : Float32 ``` The most negative finite Float32: -3.4028234663852886e38. ## module `Float64` The range of a 64-bit float, the same as a `Float`. ### `MAX` (constant) ```kex MAX : Float64 ``` The largest Float64: 1.7976931348623157e308. ### `MIN` (constant) ```kex MIN : Float64 ``` The most negative finite Float64: -1.7976931348623157e308. ## module `Int8` The range of a signed 8-bit integer. ### `MAX` (constant) ```kex MAX : Int8 ``` The largest Int8: 127. ### `MIN` (constant) ```kex MIN : Int8 ``` The smallest Int8: -128. ## module `Int16` The range of a signed 16-bit integer. ### `MAX` (constant) ```kex MAX : Int16 ``` The largest Int16: 32767. ### `MIN` (constant) ```kex MIN : Int16 ``` The smallest Int16: -32768. ## module `Int32` The range of a signed 32-bit integer. ### `MAX` (constant) ```kex MAX : Int32 ``` The largest Int32: 2147483647. ### `MIN` (constant) ```kex MIN : Int32 ``` The smallest Int32: -2147483648. ## module `Int64` The range of a signed 64-bit integer. ### `MAX` (constant) ```kex MAX : Int64 ``` The largest Int64: 9223372036854775807. ### `MIN` (constant) ```kex MIN : Int64 ``` The smallest Int64: -9223372036854775808. ## module `UInt8` The range of an unsigned 8-bit integer: `UInt8` is another name for `Byte`. ### `MAX` (constant) ```kex MAX : UInt8 ``` The largest UInt8: 255. ### `MIN` (constant) ```kex MIN : UInt8 ``` The smallest UInt8: 0. ## module `UInt16` The range of an unsigned 16-bit integer. ### `MAX` (constant) ```kex MAX : UInt16 ``` The largest UInt16: 65535. ### `MIN` (constant) ```kex MIN : UInt16 ``` The smallest UInt16: 0. ## module `UInt32` The range of an unsigned 32-bit integer. ### `MAX` (constant) ```kex MAX : UInt32 ``` The largest UInt32: 4294967295. ### `MIN` (constant) ```kex MIN : UInt32 ``` The smallest UInt32: 0. ## module `UInt64` The range of an unsigned 64-bit integer. ### `MAX` (constant) ```kex MAX : UInt64 ``` The largest UInt64: 18446744073709551615. ### `MIN` (constant) ```kex MIN : UInt64 ``` The smallest UInt64: 0. ## module `Byte` The range of a `Byte`, Kex's unsigned 8-bit integer (also called `UInt8`). ### `MAX` (constant) ```kex MAX : Byte ``` The largest Byte: 255. ### `MIN` (constant) ```kex MIN : Byte ``` The smallest Byte: 0. ## module `Number` Reading a number out of text without deciding in advance which half of the numeric tower it belongs to. ### `parse` ```kex parse(s: String) -> Result ``` Parses the whole string as an `Integer` or a `Float`, whichever the text describes. Use it when the input's shape is not known ahead of time: a config value, a CSV column that may hold either. **Parameters** - `s` — the text to parse **Returns**: the number, or a described failure **Examples** ```kex Number.parse("42") # => Ok(42) Number.parse("3.5") # => Ok(3.5) Number.parse("x") # => Error(ParseError { ... }) ``` ### `format` ```kex format(parts: [String], values: [Any]) -> NumberFormat ``` Builds a `NumberFormat` from a digit pattern, for `to(String, format)`. The pattern is checked during compilation (by `validateFormat`), so a mistake in it fails the build instead of leaving text out at run time. - `0` is a required digit: `000` pads the integer part to three digits, and `.00` always writes two decimals. - `#` is an optional digit: `#.##` writes at most two decimals and drops trailing zeros. Optional digits come before required ones in the integer part (`#0`) and after them in the decimals (`.0#`). - `.` starts the decimals. The value is rounded to as many decimals as the pattern allows, with ties away from zero. The integer part always has at least one digit, so `#.00` writes `0.50`, never `.50`. A value that rounds to zero has no minus sign. **Parameters** - `parts` — the literal pieces of the tagged literal - `values` — the interpolated values **Returns**: the format **Examples** ```kex 1.5.to(String, Number.format`#.00`) # => Just("1.50") 1.5.to(String, Number.format`#.##`) # => Just("1.5") 2.0.to(String, Number.format`#.##`) # => Just("2") 7.to(String, Number.format`000`) # => Just("007") ``` ### `validateFormat` ```kex validateFormat(source: String) -> [TaggedValidation.Issue] ``` Checks a `Number.format` pattern during compilation, pointing at the first character that does not fit. **Parameters** - `source` — the pattern **Returns**: the problem found, if any ## record `NumberFormat` How `to(String, format)` writes a number: a digit pattern such as `"#.00"`, described at `Number.format`. Build one with `Number.format`, which checks the pattern during compilation, or with `NumberFormat.parse` for a pattern known only at run time. **Fields** - `pattern` : [String](string.md#make-string) ## module `NumberFormat` ### `parse` ```kex parse(pattern: String) -> Result ``` Checks a digit pattern known only at run time. `Number.format` describes the patterns. **Parameters** - `pattern` — the pattern **Returns**: the format, or where the pattern goes wrong **Examples** ```kex NumberFormat.parse("#.00") # => Ok(NumberFormat { pattern: "#.00" }) NumberFormat.parse("#.#0") # => Error(ParseError { position: 3, ... }) ``` ### `layout` ```kex layout(pattern: String) -> Result<(Integer, Integer, Integer), (Integer, String)> ``` What a pattern writes: the fewest integer digits, the fewest decimals and the most decimals. Otherwise the index of its first problem, and what that problem is. **Parameters** - `pattern` — the pattern **Returns**: the layout, or the problem **Examples** ```kex NumberFormat.layout("#0.0##") # => Ok((1, 1, 3)) ``` ## type `Number` Implements [`Comparable`](comparable.md#trait-comparable). ### `to` ```kex to(String: Type, format: NumberFormat) -> String? ``` Writes the number by a digit pattern, described at `Number.format`. **Parameters** - `String` — the target type - `format` — the pattern **Returns**: the text, or `None` when the pattern is not valid **Examples** ```kex 1234.5.to(String, Number.format`#.00`) # => Just("1234.50") (-0.001).to(String, Number.format`#.#`) # => Just("0") 42.to(String, Number.format`0000.0`) # => Just("0042.0") ``` ### `leadingZeros` ```kex leadingZeros(text: String, width: Integer) -> String ``` `text` with leading zeros up to `width` characters. ### `trailingZeros` ```kex trailingZeros(text: String, width: Integer) -> String ``` `text` with exactly `width` characters if it is shorter, or with its trailing zeros dropped down to `width` characters if it is longer. ### From [`Comparable`](comparable.md#trait-comparable) - [`max`](comparable.md#comparable-max) — What every ordered type gets from `compare` alone. - [`min`](comparable.md#comparable-min) — Returns the smaller of this value and `other`; this value when they are equal. - [`clamp`](comparable.md#comparable-clamp) — Returns this value limited to the range `low` to `high`: `low` when it is below, `high` when it is above, and itself otherwise. - [`<`](comparable.md#comparable-op-lt) — The ordering operators, from `compare` alone: whatever is `Comparable` can be written with `<` and `>`, a record as much as a number. - [`>`](comparable.md#comparable-op-gt) — - [`<=`](comparable.md#comparable-op-lt-eq) — - [`>=`](comparable.md#comparable-op-gt-eq) — ### Defined in other modules - [Comparable](comparable.md#make-number): [`compare`](comparable.md#number-compare) --- --- package: prelude version: "0.4.0-rc.3-dev" source: optional.kex title: Optional entities: - { kind: type, name: "Optional" } - { kind: type, name: "Result" } - { kind: type, name: "Either" } - { kind: trait, name: "Optionable" } - { kind: trait, name: "Resultable" } - { kind: trait, name: "Eitherable" } - { kind: make, name: "Optional" } - { kind: make, name: "Result" } - { kind: function, name: "to" } - { kind: make, name: "Either" } --- # Optional ## type `Optional` An optional value: `Just(x)` carries a value, `None` says there is none. `X?` is shorthand for `Optional`, and is the spelling you will normally write. Kex has no `null`: anything that might not produce a value returns an `Optional` instead, so the compiler makes you say what happens when it is empty. Most of the time that is a single `.or(default)` at the end of a chain. Pattern matching handles the cases that need more than a default: **Examples** ```kex let names = ["ada", "grace"] names.first.or("nobody") # => "ada" names.at(9).or("nobody") # => "nobody" names.first.map(~upperCase) # => Just("ADA") ``` ```kex match config.get("port") do Just(port) => IO.printLine("listening on ${port}") None => IO.printLine("no port configured") end ``` **Variants** - `Just(X)` - `None` Implements [`Blankable`](blankable.md#trait-blankable), [`Showable`](kex.md#trait-showable), [`Optionable`](#trait-optionable), [`Truthyable`](truthyable.md#trait-truthyable). ### Functions #### `set?` (from Optionable) ```kex set? : Bool ``` Returns `true` when a value is present. **Examples** ```kex Just(42).set? # => true None.set? # => false ``` _Guarding on presence_ ```kex let cached = lookup(key) if cached.set? IO.printLine("hit") end ``` #### `none?` ```kex none? : Bool ``` Returns `true` when there is no value. The opposite of `set?`. **Examples** ```kex None.none? # => true Just(42).none? # => false ``` _Reporting a missing entry_ ```kex if config.get("host").none? IO.printError("host is required") end ``` #### `or` (from Optionable) ```kex or(default: X) -> X ``` Returns the wrapped value, or `default` when there is none. This is the usual way an optional leaves the optional world: put `.or` at the end of a chain and the rest of your code works with a plain value. **Parameters** - `default` — the value to use when `None` **Returns**: the wrapped value, or `default` **Examples** ```kex Just(42).or(0) # => 42 None.or(0) # => 0 ``` _Closing a chain of lookups_ ```kex ["a", "b"].first.or("?") # => "a" [].first.or("?") # => "?" "hello".indexOf('z').or(-1) # => -1 ``` #### `map` ```kex map(f: (X -> Y)) -> Y? ``` Applies `f` to the wrapped value, keeping the result wrapped. `None` is returned unchanged, so `f` never sees a missing value. **Parameters** - `f` — applied to the value when present **Returns**: `Just(f(x))`, or `None` **Examples** ```kex Just(2).map { |x| x * 3 } # => Just(6) None.map { |x| x * 3 } # => None ``` _Transforming before supplying a default_ ```kex ["ada"].first.map(~upperCase).or("ANON") # => "ADA" [].first.map(~upperCase).or("ANON") # => "ANON" ``` #### `flatMap` ```kex flatMap(f: (X -> Y?)) -> Y? ``` Applies `f`, which itself returns an optional, and flattens the result. Use it instead of `map` when the step can also fail: `map` would give you a doubly wrapped `Just(Just(x))`, `flatMap` gives a single layer. A `None` anywhere in the chain short-circuits the rest. **Parameters** - `f` — the next optional-returning step **Returns**: the result of `f`, or `None` **Examples** ```kex Just(4).flatMap { |x| x > 0 then Just(x * 2) else None } # => Just(8) Just(-4).flatMap { |x| x > 0 then Just(x * 2) else None } # => None None.flatMap { |x| Just(x * 2) } # => None ``` _Chaining lookups that may each come up empty_ ```kex users.get(id) .flatMap { |user| user.address } .flatMap { |address| address.postcode } .or("unknown") ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ### Defined in other modules - [Blankable](blankable.md#make-optional): [`blank?`](blankable.md#optional-blank?) - [Kex](kex.md#make-optional-showable): [`showValue`](kex.md#optional-showable-showvalue) - [Truthyable](truthyable.md#make-optional): [`truthy?`](truthyable.md#optional-truthy?) ## type `Result` The outcome of an operation that can fail: `Ok(x)` on success, `Error(e)` on failure with a reason. Use `Result` over `Optional` when the *reason* for failure matters to the caller. Parsing is the standard example: `"12x".to(Integer)` answers `None`, while `Integer.parse("12x")` answers an `Error` that says where it stopped. **Examples** ```kex Integer.parse("42").or(0) # => 42 Integer.parse("4x").or(0) # => 0 match Integer.parse(input) do Ok(n) => IO.printLine("got ${n}") Error(e) => IO.printError("bad number: ${e}") end ``` **Variants** - `Ok(X)` - `Error(E)` Implements [`Showable`](kex.md#trait-showable), [`Resultable`](#trait-resultable). ### Functions #### `ok?` (from Resultable) ```kex ok? : Bool ``` Returns `true` when the result is `Ok`. **Examples** ```kex Ok(42).ok? # => true Error("!").ok? # => false ``` _Counting successes_ ```kex inputs.map(~Integer.parse).count(~ok?) ``` #### `error?` ```kex error? : Bool ``` Returns `true` when the result is `Error`. The opposite of `ok?`. **Examples** ```kex Error("oops").error? # => true Ok(42).error? # => false ``` #### `or` (from Resultable) ```kex or(default: X) -> X ``` Returns the `Ok` value, or `default` when the result is an `Error`. The error payload is discarded. Match on the result instead when you need to report why it failed. **Parameters** - `default` — the value to use on failure **Returns**: the `Ok` value, or `default` **Examples** ```kex Ok(42).or(0) # => 42 Error("!").or(0) # => 0 ``` _Parsing with a fallback_ ```kex Integer.parse(input).or(8080) ``` #### `map` ```kex map(f: (X -> Y)) -> Result ``` Applies `f` to the `Ok` value. An `Error` passes through untouched, so a chain of `map` calls describes the success path only. **Parameters** - `f` — applied to the value on success **Returns**: `Ok(f(x))`, or the original `Error` **Examples** ```kex Ok(2).map { |x| x * 3 } # => Ok(6) Error("oops").map { |x| x } # => Error("oops") ``` _Converting a parsed value_ ```kex Integer.parse("21").map { |n| n * 2 } # => Ok(42) ``` #### `flatMap` ```kex flatMap(f: (X -> Result)) -> Result ``` Applies `f`, which itself returns a `Result`, and flattens the result. The step-by-step form of `map` for stages that can fail on their own. The first `Error` ends the chain and is the answer. **Parameters** - `f` — the next fallible step **Returns**: the result of `f`, or the original `Error` **Examples** ```kex Ok(4).flatMap { |x| x > 0 then Ok(x * 2) else Error("neg") } # => Ok(8) Ok(-4).flatMap { |x| x > 0 then Ok(x * 2) else Error("neg") } # => Error("neg") ``` _A pipeline where each stage may fail_ ```kex Integer.parse(raw) .flatMap { |n| n > 0 then Ok(n) else Error("must be positive") } .flatMap { |n| n < 65536 then Ok(n) else Error("out of range") } ``` #### `optional` ```kex optional : X? ``` Converts the result to an `Optional`, dropping the error payload. Useful when a caller only needs to know whether there is a value, and everything downstream already speaks `Optional`. **Returns**: `Just` of the `Ok` value, or `None` **Examples** ```kex Ok(42).optional # => Just(42) Error("oops").optional # => None ``` _Keeping only the values that parsed_ ```kex ["1", "x", "3"] .map { |s| Integer.parse(s).optional } .filter(~set?) .map { |o| o.or(0) } # => [1, 3] ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ### Defined in other modules - [Kex](kex.md#make-result): [`showValue`](kex.md#result-showvalue) ## type `Either` One of two values, of possibly different types: `Left(l)` or `Right(r)`. Unlike `Result`, neither side means failure: `Either` is for a value that is legitimately one of two shapes. **Examples** ```kex type Id = Either let describe(id: Id) -> String do match id do Left(n) => "numeric id ${n}" Right(s) => "slug id ${s}" end end ``` **Variants** - `Left(L)` - `Right(R)` Implements [`Eitherable`](#trait-eitherable). ### Functions #### `either` (from Eitherable) ```kex either(onLeft: (L -> A), onRight: (R -> A)) -> A ``` Case analysis: applies `onLeft` to a `Left` and `onRight` to a `Right`. The way an `Either` is consumed without a `match`, and the operation `Eitherable` requires. Both branches answer the same type, so the result is a plain value rather than another `Either`. **Parameters** - `onLeft` — applied to a `Left` - `onRight` — applied to a `Right` **Returns**: whichever branch ran **Examples** ```kex Left(2).either(~toString, ~upperCase) # => "2" Right("ok").either(~toString, ~upperCase) # => "OK" ``` _Collapsing an id to text_ ```kex let render(id: Either) -> String do id.either({ |n| "#${n}" }, { |slug| slug }) end ``` #### `left?` ```kex left? : Bool ``` Returns `true` for a `Left`. **Examples** ```kex Left(1).left? # => true Right(1).left? # => false ``` #### `right?` ```kex right? : Bool ``` Returns `true` for a `Right`. The opposite of `left?`. **Examples** ```kex Right(1).right? # => true Left(1).right? # => false ``` ## trait `Optionable` A value that may be absent. Constrain a generic parameter with it when a function accepts any optional value. The two operations below are what make the constraint worth having: an empty trait would accept a value and then let you do nothing with it, since there would be no function to call. `map` is deliberately NOT required — its result type differs per implementer (`Y?` here, `Result` for `Resultable`), which needs a higher-kinded parameter Kex does not have. Implemented by [`Optional`](#make-optional). ### Required functions #### `set?` ```kex set? : Bool ``` Answers `true` when a value is present. The one operation an `Optionable` type must define; `none?` is its negation. **Returns**: `true` for a present value **Examples** ```kex Just(1).set? # => true None.set? # => false ``` #### `or` ```kex or(default: X) -> X ``` Returns the wrapped value, or `default` when there is none. The way out of the optional world, and the reason a constrained parameter is usable at all. **Parameters** - `default` — the value to use when absent **Returns**: the wrapped value, or `default` **Examples** ```kex Just(42).or(0) # => 42 None.or(0) # => 0 ``` ## trait `Resultable` A value that either succeeded or failed with a reason. Constrain a generic parameter with it when a function accepts any result value. Implemented by [`Result`](#make-result). ### Required functions #### `ok?` ```kex ok? : Bool ``` Answers `true` for a success. The one operation a `Resultable` type must define; `error?` is its negation. **Returns**: `true` for `Ok` **Examples** ```kex Ok(1).ok? # => true Error("!").ok? # => false ``` #### `or` ```kex or(default: X) -> X ``` Returns the success value, or `default` on failure. **Parameters** - `default` — the value to use on failure **Returns**: the `Ok` value, or `default` **Examples** ```kex Ok(42).or(0) # => 42 Error("!").or(0) # => 0 ``` ## trait `Eitherable` One of two values, neither meaning failure. Constrain a generic parameter with it when a function accepts any either value. Implemented by [`Either`](#make-either). ### Required functions #### `either` ```kex either(onLeft: (L -> A), onRight: (R -> A)) -> A ``` Case analysis: applies `onLeft` to a `Left` and `onRight` to a `Right`. The one operation an `Eitherable` type must define — `left?` and `right?` are both written in terms of it. A discriminator alone would let you ask which side a value is on without being able to reach it, which is why this is the requirement rather than those. **Parameters** - `onLeft` — applied to a `Left` - `onRight` — applied to a `Right` **Returns**: whichever branch ran **Examples** ```kex Left(2).either(~toString, ~upperCase) # => "2" Right("ok").either(~toString, ~upperCase) # => "OK" ``` ## function `to` ```kex to(value, t: Type, radix: Integer) -> A? ``` Converts `value` to the type `t`, or `None` if it cannot be represented. `t` is a runtime type value: write the type name itself: `String`, `Integer`, `Float`, `List`. Conversion to `String` goes through the `Showable` protocol, so it works for every value. The result is an `Optional`, not a `Result`, and deliberately so: the reason a conversion failed is usually implied by the types alone. When the reason carries information (where a parse gave up and on what) reach for `Integer.parse` or `Float.parse`, which answer with `Result<_, ParseError>`. So: `to` for every day, `parse` when the failure needs handling. **Parameters** - `t` — the target type **Returns**: the converted value, or `None` **Examples** _Numbers and strings_ ```kex 42.to(String) # => Just("42") "42".to(Integer) # => Just(42) "4x".to(Integer) # => None "3.5".to(Float) # => Just(3.5) ``` _Closing the conversion immediately_ ```kex let port = env.get("PORT").flatMap { |s| s.to(Integer) }.or(8080) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: optionparser.kex title: OptionParser entities: - { kind: type, name: "OptionKind" } - { kind: record, name: "OptionSpec" } - { kind: type, name: "CommandHandler" } - { kind: record, name: "CommandSpec" } - { kind: record, name: "OptionConfig" } - { kind: record, name: "ParsedOptions" } - { kind: type, name: "OptionParseError" } - { kind: make, name: "ParsedOptions" } - { kind: make, name: "OptionConfig" } - { kind: module, name: "OptionParser" } --- # OptionParser Declarative command-line parsing, shared by Kex tools and applications. You describe the options and commands a tool accepts, and the parser turns an argument list into typed values, dispatches to the right command, and renders the help text. ```kex foul greet(options: ParsedOptions) -> Integer do IO.printLine("hello, ${options.value("name", "world")}") return 0 end main(args) do let cli = OptionParser.define("demo", "a small demo tool") .string("name", Just('n'), "who to greet", Just("world"), false) .flag("help", Just('h'), "show this help") .command("greet", "", "print a greeting", ~greet) System.exit(cli.run(args)) end $ demo greet --name Ada hello, Ada ``` Note that `--help` is not automatic: `run` shows the help when a flag named `help` is set, so declare that flag if you want it. Start at `OptionParser.define`, chain the `string` / `integer` / `flag` / `command` builders, and finish with `run` (dispatch and exit code) or `parse` (the parsed values, and nothing else). ## type `OptionKind` What kind of value an option carries: `StringValue` takes any text, `IntegerValue` must parse as a number, and `FlagValue` takes none at all and reads as `"true"` when present. **Variants** - `StringValue` - `IntegerValue` - `FlagValue` ## record `OptionSpec` One declared option. Built for you by the `OptionConfig` builders: you rarely construct one by hand. **Fields** - `long` : [String](string.md#make-string) - `short` : [Char](string.md#make-char)? (optional) - `kind` : [OptionKind](#type-optionkind) (optional) - `description` : [String](string.md#make-string) (optional) - `default` : [String](string.md#make-string)? (optional) - `required?` : [Bool](truthyable.md#make-bool) (optional) ## type `CommandHandler` What a command does once the line has been parsed. It receives the parsed options with the command's own words already removed, so `tey add greet` hands its handler `["greet"]`, and returns the process exit code. ## record `CommandSpec` One declared command. Built for you by the `OptionConfig.command` builders. **Fields** - `name` : [String](string.md#make-string) - `description` : [String](string.md#make-string) (optional) - `usage` : [String](string.md#make-string) (optional) - `handler` : [CommandHandler](#type-commandhandler) - `passthrough` : [Bool](truthyable.md#make-bool) (optional) - `section` : [String](string.md#make-string) (optional) ## record `OptionConfig` A tool's whole command-line interface: its name, its options, and its commands. Build one with `OptionParser.define` and add to it with the chainable `string`, `integer`, `flag` and `command` functions. Each returns a new `OptionConfig`, so the chain reads as one declaration. **Fields** - `name` : [String](string.md#make-string) (optional) - `description` : [String](string.md#make-string) (optional) - `options` : [[OptionSpec](#record-optionspec)] (optional) - `commands` : [[CommandSpec](#record-commandspec)] (optional) ### Functions #### `string` ```kex string(long: String, short: Char?, description: String, default: String?, required?: Bool) -> OptionConfig ``` Declares an option that takes a text value. **Parameters** - `long` — the long spelling, without dashes - `short` — an optional one-character spelling - `description` — the help-text description - `default` — the value used when the option is absent - `required?` — whether the option must be given **Returns**: the config, with the option added **Examples** ```kex OptionParser.define("demo", "a small demo tool") .string("name", Just('n'), "who to greet", Just("world"), false) ``` _An option the tool cannot run without_ ```kex config.string("output", Just('o'), "where to write", None, true) ``` #### `integer` ```kex integer(long: String, short: Char?, description: String, default: String?, required?: Bool) -> OptionConfig ``` Declares an option that takes a whole number. A value that does not parse is rejected at parse time with `InvalidInteger`, so a handler never sees a malformed number. Read it back with `ParsedOptions.integerValue`. **Parameters** - `long` — the long spelling, without dashes - `short` — an optional one-character spelling - `description` — the help-text description - `default` — the value used when the option is absent - `required?` — whether the option must be given **Returns**: the config, with the option added **Examples** ```kex config.integer("port", Just('p'), "port to listen on", Just("8080"), false) ``` #### `flag` ```kex flag(long: String, short: Char?, description: String) -> OptionConfig ``` Declares an option that takes no value and is either present or not. It defaults to `"false"` and reads as `"true"` when given. Read it back with `ParsedOptions.flagEnabled?`. Giving it a value is an error (`UnexpectedValue`). Declare a flag named `help` if you want `--help` to print the help text: `run` looks for exactly that name, and adds nothing on its own. **Parameters** - `long` — the long spelling, without dashes - `short` — an optional one-character spelling - `description` — the help-text description **Returns**: the config, with the flag added **Examples** ```kex config .flag("verbose", Just('v'), "print more detail") .flag("help", Just('h'), "show this help") ``` #### `command` ```kex command(name: String, usage: String, description: String, section: String, handler: CommandHandler) -> OptionConfig ``` Declares a command and the function that runs it. A command name may be several words (`"docs build"`) and the longest match wins, so a group and its subcommands can both be declared. The handler receives the parsed options with the command's own words already removed, and returns the process exit code. Declares a command. `usage` is what the help line shows after the name (``, `[args...]`); leave it empty for a command that takes none. **Parameters** - `name` — the command's name, one or more words - `description` — the help-text description - `handler` — what to run **Returns**: the config, with the command added **Examples** ```kex foul greet(options: ParsedOptions) -> Integer do IO.printLine("hello, ${options.value("name", "world")}") return 0 end config.command("greet", "print a greeting", ~greet) ``` #### `passthroughCommand` ```kex passthroughCommand(name: String, usage: String, description: String, handler: CommandHandler) -> OptionConfig ``` Declares a command that parses its own options. Everything after the command's name is handed to the handler in `arguments` untouched, including options this tool also declares. Use it for a subcommand backed by its own `OptionConfig`: without it, an option the outer tool happens to declare too is consumed here and never reaches the subcommand, with no error to say so. **Parameters** - `name` — the command's name, one or more words - `usage` — the argument shape shown in the help text - `description` — the help-text description - `handler` — what to run **Returns**: the config, with the command added **Examples** ```kex config.passthroughCommand("docs", "", "generate documentation", ~docs) ``` #### `parse` ```kex parse(args: [String]) -> Result ``` Parses `args` into option values and leftover words, without dispatching to a command. Use it when the tool has no commands, or when you want to inspect the parse before deciding what to do. `run` is the one-call alternative. **Parameters** - `args` — the command-line arguments **Returns**: the parse, or why it failed **Examples** ```kex match config.parse(args) do Ok(options) => IO.printLine(options.value("name", "world")) Error(e) => IO.printError(OptionParser.errorMessage(e)) end ``` #### `run` ```kex run(args: [String]) -> Integer ``` Parses `args`, runs the command they name, and returns its exit code. This is the whole of a tool's `main`: hand it the argument list and pass the result to `System.exit`. A parse failure, an unknown command, or a line with no command at all reports the problem along with the help text and returns 1: the one place that policy has to live for every tool to behave the same way. A set `help` flag prints the help and returns 0. **Parameters** - `args` — the command-line arguments **Returns**: the exit code to give the shell **Examples** ```kex main(args) do let cli = OptionParser.define("demo", "a small demo tool") .flag("help", Just('h'), "show this help") .command("greet", "", "print a greeting", ~greet) System.exit(cli.run(args)) end ``` #### `printHelp` ```kex printHelp : Integer ``` Prints the help text and returns 0, the exit code for a successful run. `run` calls this for you when the `help` flag is set; call it directly when a tool decides on its own that help is the right answer. **Returns**: always 0 **Examples** _A tool with no arguments shows its help_ ```kex args.empty? then cli.printHelp else cli.run(args) ``` #### `help` ```kex help : String ``` Returns the tool's help text: its name and description, its options, and its commands grouped by section. `run` and `printHelp` print this for you; reach for it directly when the text has to go somewhere other than stdout. The help text, styled. Every escape comes from `Console`, which the runtime blanks under `--no-colors` and `KEX_COLORS=0`, so the same code produces plain text down a pipe and this needs no second rendering path. Widths are measured on the UNSTYLED label throughout: an escape sequence has a length that a terminal does not draw, so padding computed from a coloured string lines the columns up on paper and nowhere else. **Returns**: the rendered help text **Examples** ```kex IO.printError("${OptionParser.errorMessage(error)}\n\n${cli.help}") ``` ## record `ParsedOptions` The result of a successful parse: the option values, and the words that were not options. **Fields** - `values` : {[String](string.md#make-string): [String](string.md#make-string)} - `arguments` : [[String](string.md#make-string)] ### Functions #### `value` ```kex value(name: String, default: String) -> String? ``` Returns the value given for the option named `name`, or `None` when it was neither supplied nor defaulted. **Parameters** - `name` — the option's long name, without dashes **Returns**: the value, or `None` **Examples** ```kex options.value("name") # => Just("Ada") options.value("missing") # => None ``` #### `flagEnabled?` ```kex flagEnabled?(name: String) -> Bool ``` Returns `true` when the flag named `name` was given. **Parameters** - `name` — the flag's long name, without dashes **Returns**: `true` when the flag is set **Examples** ```kex if options.flagEnabled?("verbose") IO.printLine("verbose mode") end ``` #### `integerValue` ```kex integerValue(name: String) -> Integer? ``` Returns the value of an integer option as an `Integer`, or `None` when it is absent or does not parse. An option declared with `integer` has already been checked at parse time, so `None` here means absent rather than malformed. **Parameters** - `name` — the option's long name, without dashes **Returns**: the number, or `None` **Examples** ```kex let times = options.integerValue("times").or(1) ``` ## type `OptionParseError` Why a command line could not be parsed. `OptionParser.errorMessage` turns one into a sentence for the user; `OptionConfig.run` does that for you. **Variants** - `UnknownOption(String)` - `MissingValue(String)` - `UnexpectedValue(String)` - `MissingRequired(String)` - `InvalidInteger(String, String)` ## module `OptionParser` ### `define` ```kex define(name: String, description: String) -> OptionConfig ``` Starts an immutable command-line interface definition. Chain options and commands onto the returned `OptionConfig`, then call `parse` with the process arguments. `name` appears in usage text and `description` introduces the generated help page. **Parameters** - `name` — the executable name shown in usage - `description` — the one-line purpose of the command **Returns**: an empty parser configuration **Examples** _Defining a small file-conversion command_ ```kex let cli = OptionParser.define("convert", "Convert a document to PDF") .string("input", Just('i'), "Source document", None, true) .flag("verbose", Just('v'), "Show conversion details") ``` ### `commandLabel` ```kex commandLabel(command: CommandSpec) -> String ``` Returns the label a command is listed under in the help text: its name, followed by its usage when it has one. **Parameters** - `command` — the command to label **Returns**: the help-text label **Examples** ```kex OptionParser.commandLabel(spec) # => "build " ``` ### `commandFor` ```kex commandFor(commands: [CommandSpec], arguments: [String]) -> (CommandSpec, [String])? ``` Finds the declared command whose words open `arguments`, and returns it with whatever is left after them. Longest match first, so `"kex install"` is preferred over a `"kex"` that also exists. Answers `None` when no command matches. **Parameters** - `commands` — the declared commands - `arguments` — the positional words from the command line **Returns**: the command and the remaining words **Examples** ```kex OptionParser.commandFor(commands, ["docs", "build", "src"]) # => Just((docsBuildSpec, ["src"])) ``` ### `passthrough?` ```kex passthrough?(commands: [CommandSpec], arguments: [String]) -> Bool ``` Whether the command `arguments` names parses its own options. `false` when no command matches yet, so options before any command word are still this parser's to claim. **Parameters** - `commands` — the declared commands - `arguments` — the positional words seen so far **Returns**: `true` when a matched command is passthrough **Examples** ```kex OptionParser.passthrough?(commands, ["docs", "build"]) # => true OptionParser.passthrough?(commands, []) # => false ``` ### `opensWith?` ```kex opensWith?(arguments: [String], name: String) -> Bool ``` Returns `true` when `arguments` begins with the words of `name`. The word-wise prefix test `commandFor` matches with: `"docs build"` opens `["docs", "build", "src"]` but not `["docs"]`. **Parameters** - `arguments` — the positional words from the command line - `name` — the command name, one or more words **Returns**: `true` when the words match at the front **Examples** ```kex OptionParser.opensWith?(["docs", "build"], "docs") # => true OptionParser.opensWith?(["docs"], "docs build") # => false ``` ### `errorMessage` ```kex errorMessage(error: OptionParseError) -> String ``` Renders a parse error as a sentence for the user. `OptionConfig.run` does this for you; call it directly when handling a `parse` result yourself. **Parameters** - `error` — the failure to describe **Returns**: the message to show **Examples** ```kex OptionParser.errorMessage(UnknownOption("--nope")) # => "unknown option '--nope'" OptionParser.errorMessage(MissingValue("output")) # => "--output requires a value" ``` ### `parse` ```kex parse(options: [OptionSpec], commands: [CommandSpec], args: [String]) -> Result ``` Parses `args` against a list of option and command specs. The engine behind `OptionConfig.parse`, which is the form to call from ordinary code. **Parameters** - `options` — the declared options - `commands` — the declared commands - `args` — the command-line arguments **Returns**: the parse, or why it failed --- --- package: prelude version: "0.4.0-rc.3-dev" source: parsing.kex title: Parsing entities: - { kind: module, name: "Parsing" } --- # Parsing ## module `Parsing` Parser combinators, for reading a text format you define yourself. Opt-in: nothing here is in scope until `using Parsing`. An `Input` is an immutable cursor over a string. A parser takes one and answers either the value it read together with an advanced cursor, or a `ParseError` saying where it gave up. Because the cursor is immutable, a failed attempt costs nothing: the caller still holds the position it started from and can try something else. ```kex using Parsing main do let cursor = Input { input: "abc 123" } let word = cursor.takeWhile(~alpha?) let (text, rest) = word IO.printLine(text) # prints: abc IO.printLine(rest.pos) # prints: 3 end ``` The building blocks are `char` and `charWhen` for one character, `string` for a literal, `takeWhile` for a run, and `many` / `some` / `choice` for repetition and alternatives. `JSON` in this same stdlib is written with them. ## type `ParseError` Why a parser gave up, and at which position. `Expected` carries what the grammar WANTED rather than what it found: the difference between "unexpected `d`" and "expected `version(`". It is what `label` and `string` report. ```kex Input { input: "abc" }.char('z') # => Error(Unexpected("a", 0)) Input { input: "abc" }.string("abd") # => Error(Expected("abd", 0)) ``` **Variants** - `Unexpected(String, Integer)` - `Expected(String, Integer)` - `NoMatch(Integer)` ## record `Input` An immutable cursor over a string: the text, and where in it you are. Create one with `Input { input: text }` and pass it to a parser. Every operation that consumes input answers a NEW cursor rather than moving this one, which is what makes backtracking free. **Fields** - `input` : [String](string.md#make-string) - `pos` : [Integer](number.md#make-integer) (optional) ### Functions #### `peek` The character at the cursor, or `None` at the end of the input. Looking does not consume: the cursor is unchanged. **Returns**: `Char?` — the current character, or `None` **Examples** ```kex Input { input: "abc" }.peek # => Just('a') Input { input: "abc", pos: 3 }.peek # => None ``` #### `peekAt` ```kex peekAt(offset: Integer) -> Char? ``` The character `offset` positions ahead of the cursor, or `None` when that is outside the input. The lookahead a grammar needs when one character is not enough to decide. A negative offset looks backwards. **Parameters** - `offset` — how far ahead to look **Returns**: the character there, or `None` **Examples** ```kex Input { input: "abc" }.peekAt(1) # => Just('b') Input { input: "abc" }.peekAt(9) # => None ``` #### `atEnd?` Returns `true` when the cursor has consumed the whole input. The check a top-level parser makes at the end, to be sure nothing was left over. **Returns**: `Bool` — `true` at the end of the input **Examples** ```kex Input { input: "abc" }.atEnd? # => false Input { input: "abc", pos: 3 }.atEnd? # => true ``` #### `advance` A cursor one character further on. **Returns**: `Input` — the advanced cursor **Examples** ```kex Input { input: "abc" }.advance.peek # => Just('b') ``` #### `advanceBy` ```kex advanceBy(count: Integer) -> Input ``` A cursor `count` characters further on. **Parameters** - `count` — how far to advance **Returns**: the advanced cursor **Examples** ```kex Input { input: "abc 123" }.advanceBy(4).peek # => Just('1') ``` #### `remaining` Everything from the cursor to the end of the input, as a `String`. Useful for reporting an error, or for handing the tail to something that does not speak `Input`. **Returns**: `String` — the unconsumed remainder **Examples** ```kex Input { input: "abc 123", pos: 4 }.remaining # => "123" ``` #### `charWhen` ```kex charWhen(pred: (Char -> Bool)) -> Result<(Char, Input), ParseError> ``` Reads one character, if it satisfies `pred`. Answers the character and the advanced cursor, or an `Unexpected` error naming what was there instead: `"EOF"` at the end of the input. **Parameters** - `pred` — the test the character must pass **Returns**: the character and cursor, or why not **Examples** ```kex Input { input: "abc" }.charWhen(~alpha?) # => Ok(('a', cursor at 1)) Input { input: "123" }.charWhen(~alpha?) # => Error(Unexpected("1", 0)) ``` _Reading one digit_ ```kex cursor.charWhen(~digit?) ``` #### `char` ```kex char(expected: Char) -> Result<(Char, Input), ParseError> ``` Reads one specific character. **Parameters** - `expected` — the character that must be next **Returns**: the character and cursor, or why not **Examples** ```kex Input { input: "abc" }.char('a') # => Ok(('a', cursor at 1)) Input { input: "abc" }.char('z') # => Error(Unexpected("a", 0)) ``` _Consuming a separator_ ```kex let (_, afterComma) = cursor.char(',').try ``` #### `whiteSpaces` ```kex whiteSpaces : Input ``` A cursor advanced past any run of whitespace. Cannot fail: no whitespace at all leaves the cursor where it was, which is what makes it safe to call between every token of a grammar. **Returns**: the cursor, past the whitespace **Examples** ```kex Input { input: "abc 123", pos: 3 }.whiteSpaces.peek # => Just('1') ``` _Skipping space between tokens_ ```kex let (value, rest) = parseValue(cursor.whiteSpaces).try ``` #### `many` ```kex many(f: (Input -> Result<(T, Input), ParseError>)) -> ([T], Input) ``` Applies `f` as many times as it succeeds, collecting the results. Cannot fail: zero matches is an empty list, which is what makes it right for the optional parts of a grammar. A parser that succeeds without consuming anything stops the loop rather than spinning forever. **Parameters** - `f` — the parser to repeat **Returns**: the results, and the cursor after them **Examples** ```kex Input { input: "abc 1" }.many { |p| p.charWhen(~alpha?) } # => (['a', 'b', 'c'], cursor at 3) ``` _Zero matches is not an error_ ```kex Input { input: "123" }.many { |p| p.charWhen(~alpha?) } # => ([], cursor at 0) ``` #### `some` ```kex some(f: (Input -> Result<(T, Input), ParseError>)) -> Result<([T], Input), ParseError> ``` Applies `f` at least once, then as many more times as it succeeds. The one-or-more counterpart of `many`: the first failure IS a failure, so use it where the grammar requires something to be there. **Parameters** - `f` — the parser to repeat **Returns**: the results and the cursor, or the first failure **Examples** ```kex Input { input: "abc" }.some { |p| p.charWhen(~alpha?) } # => Ok((['a', 'b', 'c'], cursor at 3)) Input { input: "123" }.some { |p| p.charWhen(~alpha?) } # => Error(Unexpected("1", 0)) ``` #### `string` ```kex string(expected: String) -> Result<(String, Input), ParseError> ``` Reads an exact literal. A keyword grammar is mostly literals: `version(` is one token to a reader and eight calls to `char`, and matching it here reports the failure at the START of the literal, which is where a person looking at the error expects the caret. **Parameters** - `expected` — the literal that must be next **Returns**: the literal and the cursor, or an `Expected` error **Examples** ```kex Input { input: "abc" }.string("abc") # => Ok(("abc", cursor at 3)) Input { input: "abc" }.string("abd") # => Error(Expected("abd", 0)) ``` _Matching a keyword_ ```kex let (_, rest) = cursor.string("version(").try ``` #### `takeWhile` ```kex takeWhile(pred: (Char -> Bool)) -> (String, Input) ``` Reads every character while `pred` holds, as a `String`. `many(charWhen(...))` gives a [Char] the caller has to join, and a run of characters is almost always wanted as text. Cannot fail: an empty run is an empty String, which is what makes it safe for the optional parts of a grammar. **Parameters** - `pred` — the test each character must pass **Returns**: the text read, and the cursor after it **Examples** ```kex Input { input: "abc 123" }.takeWhile(~alpha?) # => ("abc", cursor at 3) Input { input: "123" }.takeWhile(~alpha?) # => ("", cursor at 0) ``` _Reading an identifier_ ```kex let name = cursor.takeWhile { |c| c.alpha? || c == '_' } ``` #### `choice` ```kex choice(alts: ([(Input) -> Result<(T, Input), ParseError>])) -> Result<(T, Input), ParseError> ``` Tries each parser in `alts` in turn, and answers the first that succeeds. This is alternation: how a grammar says "a value is a string, or a number, or an object". Because the cursor is immutable, a failed alternative costs nothing. When none of them match, the answer is `NoMatch` at the position they all started from. **Parameters** - `alts` — the parsers to try, in order **Returns**: the first success, or `NoMatch` **Examples** ```kex cursor.choice([ { |p| p.charWhen(~digit?) }, { |p| p.charWhen(~alpha?) } ]) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: prelude.kex title: Prelude entities: --- # Prelude The standard-library surface automatically visible to every Kex source file. Prelude names require no `using` at the call site. This includes everyday values and operations (`List`, `Map`, `String`, `Math`, `IO`), language-level protocols (`Enumerable`, `Truthyable`, `Errorable`), and core runtime services such as processes and time. For example, `.map`, conditions, and `.try` work without importing the traits that define them. Being useful is not enough for inclusion. Every prelude name occupies global scope in every program, so a module belongs here only when ordinary Kex code needs it pervasively or the language model depends on it. Modules stay opt-in when they expose a capability that should be visible in a file's dependencies, or when most programs do not need their vocabulary. Examples include the filesystem (`FS`), regular expressions (`Regex`), JSON, parser combinators (`Parsing`), bit operations (`Bits`), data structures such as `Data.Set`, hashing (`Digest`), command-line parsing (`OptionParser`), and the sandboxed evaluator (`Evaluator`). ```kex using FS main do let config = FS.File.read("config.txt").or("") IO.printLine(config) end ``` Keep this file declarative. Each bare `using` names a sibling stdlib source; the toolchain expands those sources in order when it builds or loads the prelude. Put documentation for an individual type or operation beside its declaration in that source file, not here. --- --- package: prelude version: "0.4.0-rc.3-dev" source: process.kex title: Process entities: - { kind: type, name: "Pid" } - { kind: type, name: "Task" } - { kind: type, name: "Reference" } - { kind: type, name: "Process" } - { kind: type, name: "ProcessExitReason" } - { kind: record, name: "Reply" } - { kind: type, name: "From" } - { kind: type, name: "CallError" } - { kind: record, name: "Server" } - { kind: module, name: "Process" } - { kind: record, name: "ProcessResult" } - { kind: make, name: "Pid" } - { kind: make, name: "Process" } - { kind: make, name: "Server" } - { kind: make, name: "From" } - { kind: module, name: "Task" } - { kind: make, name: "Task" } - { kind: module, name: "Supervisor" } - { kind: make, name: "Reference" } --- # Process Processes, tasks and external commands. Kex's concurrency is the BEAM's: lightweight processes that share nothing and communicate by message. There are three things here, and they answer different questions. `Task` runs one piece of work somewhere else and gives you the answer back: ```kex let a = Task.start do expensiveThing(1) end let b = Task.start do expensiveThing(2) end (a.await, b.await) ``` `serving` plus `Process.spawn` gives a piece of state its own process, with typed calls into it: ```kex record RateLimiter do remaining : Integer end serving RateLimiter do slot allowed? -> Reply do let allowed = @remaining > 0 new.remaining = allowed then @remaining - 1 else @remaining return { new, reply: allowed } end end let api = Process.spawn(RateLimiter { remaining: 2 }) api.allowed?() # => Ok(true) ``` `Process.run` and `Process.stream` run an external program. ```kex Process.run("git", ["rev-parse", "HEAD"]) ``` Backed by Kex.Intrinsic.Process and the BEAM runtime. ## type `Pid` An opaque BEAM process identifier. Obtained from `Process.self` or `Process.whereIs`. Send it messages, link to it, monitor it, or ask whether it is still alive. ### Functions #### `send` ```kex send(msg: X) -> Void ``` Sends `msg` to the process, unchanged, as a raw BEAM term. Sending never blocks and never fails, even when the process is gone: that is the BEAM's model, not an oversight. Monitor the process when delivery matters. **Parameters** - `msg` — the message to send **Examples** ```kex worker.send(:stop) worker.send(("job", 42)) ``` #### `sendFrom` ```kex sendFrom(msg: X) -> Void ``` Sends the conventional Erlang sender-bearing pair `{Process.self, msg}`, so the receiver knows where to answer. **Parameters** - `msg` — the message to send **Examples** ```kex server.sendFrom(:status) # the server receives (senderPid, :status) ``` #### `link` ```kex link : Void ``` Links the calling process to this one, so that either one exiting abnormally takes the other down. Linking is bidirectional and is how a group of processes is made to fail together. Use `monitor` when you want to be told about a death without sharing it. **Examples** ```kex worker.link ``` #### `unlink` ```kex unlink : Void ``` Removes the link between the calling process and this one. **Examples** ```kex worker.unlink ``` #### `monitor` ```kex monitor : Reference ``` Starts monitoring this process, and returns the reference that identifies the monitor. When the monitored process exits, the caller receives a message about it. Unlike `link`, a monitor is one-directional and does not propagate the exit. Pass the reference to `demonitor` to stop. **Returns**: the monitor reference **Examples** ```kex let ref = worker.monitor ref.demonitor ``` #### `alive?` ```kex alive? : Bool ``` Returns `true` when the process is still running. Inherently a snapshot: the process may exit immediately after you ask. Monitor it when the answer has to stay true. **Returns**: `true` when the process is alive **Examples** ```kex Process.self.alive? # => true ``` ## type `Task` A handle on work running in another process, with a result you can await. Created by `Task.start`. ### Functions #### `await` ```kex await : Result ``` Waits for the task's result, for as long as it takes. **Returns**: the result or failure reason **Examples** ```kex Task.start do 1 + 1 end.await # => Ok(2) ``` ```kex await(timeout: Integer) -> Result ``` Waits for the task's result, giving up after `timeout` milliseconds. Answers `Error(:timeout)` if the task has not finished in time. The task itself is not stopped. **Parameters** - `timeout` — how long to wait, in milliseconds **Returns**: the result or failure reason **Examples** ```kex let t = Task.start do slowThing() end t.await(1000) ``` ## type `Reference` A monitor reference, returned by `monitor` and passed back to `demonitor`. ### Functions #### `demonitor` ```kex demonitor : Void ``` Stops the monitor this reference identifies. **Examples** ```kex let ref = worker.monitor ref.demonitor ``` ## type `Process` A typed process handle: like a `Pid`, but it remembers what kind of message the process accepts. ### Functions A spawned Process is a typed process handle backed by the same runtime pid. It therefore supports the ordinary pid lifecycle operations without erasing its message type. #### `send` ```kex send(msg: X) -> Void ``` Sends `msg` to the process. Unlike `Pid.send`, the message type is checked. **Parameters** - `msg` — the message to send #### `sendFrom` ```kex sendFrom(msg: X) -> Void ``` Sends the sender-bearing pair `{Process.self, msg}`, with the message type checked. **Parameters** - `msg` — the message to send #### `link` ```kex link : Void ``` Links the calling process to this one. See `Pid.link`. #### `unlink` ```kex unlink : Void ``` Removes the link between the calling process and this one. #### `monitor` ```kex monitor : Reference ``` Starts monitoring this process. See `Pid.monitor`. **Returns**: the monitor reference #### `alive?` ```kex alive? : Bool ``` Returns `true` when the process is still running. **Returns**: `true` when the process is alive ## type `ProcessExitReason` A process termination reason. This remains open because the BEAM permits any term as an exit reason; conventional values include `:normal`, `:shutdown`, and structured application errors. **Variants** - `Any` ## record `Reply` What a synchronous `slot` returns: an answer for the caller, and optionally a new state and a reason to stop. `reply` answers the caller immediately. A slot may omit it only when it sends a deferred response with `from.reply(...)`. `new` installs the next serving state; omitting it preserves the current state. `stop` terminates the server after applying the transition. Within a `serving X` block, the checker narrows `new` from `Any?` to `X?`. ```kex slot allowed? -> Reply do let allowed = @remaining > 0 new.remaining = allowed then @remaining - 1 else @remaining return { new, reply: allowed } end slot stop -> Reply = { stop: :normal, reply: @remaining } ``` **Fields** - `reply` : A - `new` : Any? (optional) - `stop` : [ProcessExitReason](#type-processexitreason)? (optional) ## type `From` The identity of a pending caller, for a slot that answers later rather than immediately. Hand it `reply` when the answer is ready. ### Functions #### `reply` ```kex reply(value: X) -> Void ``` Answers the pending call this value identifies. A slot that cannot answer immediately, because it is waiting on something else: omits `reply` from its transition and calls this later instead. **Parameters** - `value` — the answer to send **Examples** _Answering after the work is done_ ```kex Task.start do from.reply(expensiveThing()) end ``` #### `pid` ```kex pid : Pid ``` Returns the waiting caller's `Pid`. **Returns**: the caller ## type `CallError` Why a call into a server failed: it took too long, the process is gone, or it crashed handling the call. **Variants** - `Timeout` - `NoProcess` - `CallFailed` ## record `Server` A running server and the default timeout for calls into it. Returned by `Process.spawn`. Every `slot` declared in the type's `serving` block exposes a function taking this `Server` as its receiver and returning a `Result`. **Fields** - `process` : [Process](#type-process) - `timeout` : [Integer](number.md#make-integer) (optional) ### Functions #### `within` ```kex within(timeout: Integer) -> Server ``` Returns the same server with a different default call timeout, in milliseconds. The server is untouched: this is a new view of it, so one slow call can be given more room without changing anything for other callers. **Parameters** - `timeout` — the call timeout in milliseconds **Returns**: a view of the server with that timeout **Examples** _Giving one call longer to answer_ ```kex api.within(30000).rebuildIndex() ``` #### `link` ```kex link : Void ``` Links the calling process to the server. See `Pid.link`. **Examples** ```kex api.link ``` #### `unlink` ```kex unlink : Void ``` Removes the link between the calling process and the server. #### `monitor` ```kex monitor : Reference ``` Starts monitoring the server. See `Pid.monitor`. **Returns**: the monitor reference #### `alive?` ```kex alive? : Bool ``` Returns `true` when the server is still running. **Returns**: `true` when the server is alive **Examples** ```kex api.alive? # => true ``` ## module `Process` Spawning servers, running external commands, and the ambient process operations. ### `spawn` ```kex spawn(state: X) -> Server ``` Starts a process running the `serving` implementation attached to `state`'s type, and returns a handle on it. The state you pass is the server's initial state. Every `slot` in the `serving` block exposes a function taking the returned `Server` as its receiver. Each returns a `Result`: a call can time out or find the process gone. **Parameters** - `state` — the server's initial state **Returns**: a handle on the running server **Examples** ```kex let api = Process.spawn(RateLimiter { remaining: 2 }) api.allowed?() # => Ok(true) api.allowed?() # => Ok(true) api.allowed?() # => Ok(false) api.stop() ``` ### `run` ```kex run(command: String, args: [String]) -> Result run(command: String, args: [String]) -> Integer -> Result ``` Runs an executable with an argument vector and captures its output. No shell is involved, so nothing is glob-expanded or word-split and arguments containing spaces need no quoting. Output is captured as UTF-8 strings. A non-zero exit status is still `Ok`: the program ran and said something, which is information, not a failure to run it. `Error` means the child could not be started at all. **Parameters** - `command` — the executable to run - `args` — its arguments, one per element **Returns**: the captured result, or why it could not start **Examples** ```kex match Process.run("echo", ["hello"]) do Ok(r) => IO.printLine(r.stdout.trim) # prints: hello Error(e) => IO.printError(e) end ``` _A non-zero status is still Ok_ ```kex Process.run("false", []) # => Ok(ProcessResult { exitCode: 1, ... }) ``` _A command that does not exist is an Error_ ```kex Process.run("no-such-command", []) # => Error("executable not found: no-such-command") ``` _Reading a command's output as data_ ```kex Process.run("git", ["rev-parse", "HEAD"]) .map { |r| r.stdout.trim } .or("unknown") ``` ### `stream` ```kex stream(command: String, args: [String]) -> Result ``` Runs an executable with the CALLER's stdin, stdout and stderr, so its output appears as it is produced rather than in one block when it exits, and a child that reads the terminal — a REPL, a prompt — has it. Answers the exit code; nothing is captured: that is the trade, and it is useful for commands whose progress should appear in the terminal. `run` remains the one to use when the output is data to be READ. **Parameters** - `command` — the executable to run - `args` — its arguments, one per element **Returns**: the exit code, or why it could not start **Examples** _Watching a build as it runs_ ```kex match Process.stream("make", ["-j8"]) do Ok(0) => IO.printLine("build succeeded") Ok(code) => IO.printError("build failed with ${code}") Error(e) => IO.printError(e) end ``` ### `exec` ```kex exec(command: String, args: [String]) -> Integer ``` Runs an executable and returns just its exit code, discarding its output. A command that could not be started answers `127`, the shell's convention for "command not found". Use `run` when you need the output or want to tell a failed start from a failed run. **Parameters** - `command` — the executable to run - `args` — its arguments, one per element **Returns**: the exit code, or `127` if it could not start **Examples** ```kex Process.exec("true", []) # => 0 Process.exec("false", []) # => 1 Process.exec("no-such-command", []) # => 127 ``` _Testing for a tool's presence_ ```kex let haveGit = Process.exec("git", ["--version"]) == 0 ``` ### `self` ```kex self : Pid ``` Returns the calling process's own `Pid`. **Returns**: this process's identifier **Examples** _Telling another process where to answer_ ```kex worker.send((Process.self, :ping)) ``` ### `exit` ```kex exit(pid: Pid, reason: X) -> Void ``` Sends an exit signal carrying `reason` to `pid`. `:normal` is the ordinary shutdown reason; `:kill` cannot be trapped. **Parameters** - `pid` — the process to signal - `reason` — the exit reason **Examples** ```kex Process.exit(worker, :shutdown) ``` ### `register` ```kex register(pid: Pid, name: Atom) -> Void register(pid: Process, name: Atom) -> Void ``` Registers `pid` under the atom `name`, so it can be found by name rather than by passing the `Pid` around. **Parameters** - `pid` — the process to register - `name` — the name to register it under **Examples** ```kex Process.register(Process.self, :main) ``` ### `whereIs` ```kex whereIs(name: Atom) -> Pid? ``` Returns the `Pid` registered under `name`, or `None` when nothing is. **Parameters** - `name` — the registered name **Returns**: the process, or `None` **Examples** ```kex Process.whereIs(:main) # => Just(pid) Process.whereIs(:not_there) # => None ``` _Sending to a named process if it is there_ ```kex Process.whereIs(:logger).map { |pid| pid.send(message) } ``` ## type `Shared` A named, typed slot of state that every process can read without asking another process for it. State kept in a `serving` process is copied into the caller on every read, which is costly for a large value read by every request. A `Shared` value is stored once for the whole node (on the BEAM, in `persistent_term`): reading it copies nothing, however large it is. The price is on the other side: replacing a value makes the runtime scan every process for references to the old one. So it suits state that is read constantly and written rarely, such as configuration or a loaded site, and not a counter. Get a handle with `Process.Shared.named`. The slot is identified by its name alone, so two handles with the same name are the same slot; the type parameter is what a handle promises to store and read back, so give every handle on one name the same one. **Examples** _Loading once, reading from every request handler_ ```kex let site : Process.Shared = Process.Shared.named("site") site.put(loadSite(root)) ... let current = site.get.or(emptySite) ``` ### Functions #### `put` ```kex put(value: X) -> Void ``` Stores `value`, replacing whatever the slot held. Expensive: see `Process.Shared`. **Parameters** - `value` — the value to store #### `get` ```kex get : X? ``` The stored value, or `None` when nothing has been stored yet. Cheap, whatever the value's size. **Returns**: the stored value #### `delete` ```kex delete : Bool ``` Empties the slot. **Returns**: `true` when it held a value ## module `Process.Shared` ### `named` ```kex named(name: String) -> Process.Shared ``` The handle on the slot called `name`. Nothing is stored or read until `put` or `get`. **Parameters** - `name` — the slot's name **Returns**: a handle on the slot ## record `ProcessResult` What an external command left behind: its exit status and its output. Returned inside `Ok` by `Process.run`, whatever the exit status. **Fields** - `exitCode` : [Integer](number.md#make-integer) - `stdout` : [String](string.md#make-string) - `stderr` : [String](string.md#make-string) ## module `Task` Running work in another process and collecting the answer. ### `sleep` ```kex sleep(duration: Duration) -> Void ``` Suspends for an elapsed duration. Negative durations are treated as zero. ### `start` ```kex start(f: Block) -> Task ``` Runs `f` in a new process and returns a handle on its result. The block starts immediately, so starting several tasks and awaiting them afterwards is what makes them run at the same time. **Parameters** - `f` — the work to run **Returns**: a handle on the result **Examples** ```kex let t = Task.start do 1 + 1 end t.await # => Ok(2) ``` _Two pieces of work at once_ ```kex let a = Task.start do slowThing("a") end let b = Task.start do slowThing("b") end (a.await, b.await) ``` ### `awaitAll` ```kex awaitAll(tasks: [Task]) -> [Result] ``` Waits for every task in `tasks` and returns their results, in the order the tasks were given. Each result comes back wrapped in a `Result`, so one task failing does not cost you the others' answers, just as with `await` on a single task. **Parameters** - `tasks` — the tasks to wait for **Returns**: their results, in order **Examples** ```kex let tasks = [Task.start do 1 end, Task.start do 2 end] Task.awaitAll(tasks) # => [Ok(1), Ok(2)] ``` _Fanning work out over a list_ ```kex let results = Task.awaitAll(paths.map { |p| Task.start do process(p) end }) ``` ## module `Supervisor` Supervision trees: processes that restart the processes under them when they crash. ### `start` ```kex start(restart: Atom, children: Block<[(Atom, Block)]>) -> Result start(restart: Atom, children: [(Atom, Block)]) -> Result ``` Starts a supervisor over the children its block lists, one per line — +Supervisor.worker+s and nested +Supervisor.supervisor+s. When a child crashes, `restart` decides which of its siblings restart with it: - `:only_crashed` — just the crashed child - `:crashed_and_newer` — it and the children listed after it - `:all` — every child The interpreter supports `:only_crashed` without nesting only. **Parameters** - `restart` — the restart strategy - `children` — the block listing the children **Returns**: the supervisor, or why it failed to start **Examples** ```kex using Supervisor start(restart: :only_crashed) do worker { startCache() } supervisor(restart: :all) do worker { startPool() } worker { startListener() } end end ``` ### `worker` ```kex worker(start: Block) -> (Atom, Block) ``` A child that runs the block to start, and again to restart. The block must spawn the worker and return its `Pid`. **Parameters** - `start` — the block that spawns the worker **Returns**: the worker spec **Examples** ```kex Supervisor.worker { spawn do cacheLoop([]) end } ``` ### `supervisor` ```kex supervisor(restart: Atom, children: Block<[(Atom, Block)]>) -> (Atom, Block) supervisor(restart: Atom, children: [(Atom, Block)]) -> (Atom, Block) ``` A child that is itself a supervisor over the children its block lists, one per line. Its parent starts it, and restarts it — with all its children — when the parent's restart strategy says so. Takes the same `restart:` strategies as `Supervisor.start`. BEAM backend only. **Parameters** - `restart` — `:only_crashed`, `:all` or `:crashed_and_newer` - `children` — the block listing its children **Returns**: the supervisor spec **Examples** ```kex Supervisor.supervisor(restart: :all) do Supervisor.worker { startPool() } Supervisor.worker { startListener() } end ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: random.kex title: Random entities: - { kind: module, name: "Random" } --- # Random ## module `Random` Random values for simulations, sampling, games, and reproducible tests. Import with `using Random`. Ordinary calls return a value directly; a `Random.Generator` returns `(value, nextGenerator)`. Reusing the same generator replays the same draw. Pass its returned state to continue. These generators are not cryptographic; do not use their output for secrets. `Random.secureBytes` and `Random.token` are, and are what secrets come from. **Examples** _Roll a die or initialize a weight_ ```kex using Random let roll = Random.integer(1..6) let weight = Random.float(-1.0..1.0) ``` _Reproduce a sequence_ ```kex let generator = Random.seeded(42) let (first, afterFirst) = generator.integer(1..6) let (second, afterSecond) = afterFirst.integer(1..6) ``` ### `seeded` ```kex seeded(seed: Integer) -> Generator ``` Starts a deterministic sequence. Any Integer seed is reduced modulo 2^64. **Parameters** - `seed` — reproducible seed, including negative or large values **Returns**: initial state; no draw has been consumed ### `fresh` ```kex fresh : Generator ``` Starts a generator using fresh host entropy. **Returns**: initial state for a sequence that varies between runs ### `integer` ```kex integer(range: Range) -> Integer ``` Draws an integer from an inclusive range using fresh entropy. **Parameters** - `range` — ascending inclusive bounds, span at most 2^64 **Returns**: drawn value ### `float` ```kex float(range: Range = …) -> Float ``` Draws a float; defaults to [0.0, 1.0). The upper bound is excluded. **Parameters** - `range` — finite, strictly ascending bounds **Returns**: drawn value ### `boolean` ```kex boolean : Bool ``` Draws a fair coin flip using fresh entropy. **Returns**: true or false with equal probability ### `chance?` ```kex chance?(probability: Float) -> Bool ``` Returns true with the supplied probability using fresh entropy. **Parameters** - `probability` — probability from 0.0 through 1.0 **Returns**: sampled outcome ### `choice` ```kex choice(items: [X]) -> X? ``` Chooses a position uniformly, returning None for an empty input. **Parameters** - `items` — values to choose from **Returns**: chosen value, if any ### `sample` ```kex sample(items: [X], count: Integer) -> [X] ``` Samples positions without replacement; duplicate values remain possible. **Parameters** - `items` — population - `count` — nonnegative count; capped at the input size **Returns**: values selected in random order ### `shuffle` ```kex shuffle(items: [X]) -> [X] ``` Returns a random permutation using fresh entropy. **Parameters** - `items` — values to shuffle **Returns**: shuffled copy ### `secureBytes` ```kex secureBytes(count: Integer) -> Binary ``` Returns `count` bytes from the operating system's cryptographically secure generator: the source for keys, session tokens and unguessable identifiers, which the generators above must never be used for. Nothing about it is reproducible, and there is no `Generator` form. **Parameters** - `count` — how many bytes; zero or less gives an empty binary **Returns**: the random bytes **Examples** _A 256-bit key_ ```kex let key = Random.secureBytes(32) ``` ### `token` ```kex token(bytes: Integer = …) -> String ``` Returns an unguessable token: `bytes` secure random bytes, as lowercase hex, so the text is twice as long as `bytes`. **Parameters** - `bytes` — how many random bytes the token carries **Returns**: the token **Examples** _A session identifier_ ```kex let session = Random.token(32) # => "9f86d081884c7d65..." (64 characters) ``` ## record `Generator` Immutable state of a deterministic random sequence. Construct with `Random.seeded` or `Random.fresh`. Each draw returns its value first and the advanced generator second. Equal seeds reproduce equal sequences for the same operations on both backends. **Fields** - `state` : [Integer](number.md#make-integer) (optional) ## module `Random.Generator` ## type `Random.Generator` ### `integer` ```kex integer(range: Range) -> (Integer, Generator) ``` Draws an integer between both endpoints, with the advanced generator. The range must be ascending and contain at most 2^64 integers. A singleton range is valid and still consumes a draw. **Parameters** - `range` — inclusive integer bounds **Returns**: drawn integer and advanced state **Examples** _A reproducible die roll_ ```kex let (roll, advanced) = Random.seeded(42).integer(1..6) ``` ### `float` ```kex float(range: Range) -> (Float, Generator) ``` Draws a float from the lower bound inclusive to the upper bound exclusive. Defaults to `0.0..1.0`. Bounds must be finite and strictly ascending. **Parameters** - `range` — half-open floating-point bounds **Returns**: drawn float and advanced state **Examples** _Initialize a model weight_ ```kex let (weight, advanced) = Random.seeded(42).float(-1.0..1.0) ``` ### `boolean` ```kex boolean : (Bool, Generator) ``` Draws a fair Boolean and returns the advanced generator. **Returns**: coin flip and advanced state ### `chance?` ```kex chance?(probability: Float) -> (Bool, Generator) ``` Draws true with the supplied probability. Consumes one draw even at 0 or 1. **Parameters** - `probability` — probability from 0.0 through 1.0 **Returns**: outcome and advanced state ### `choice` ```kex choice(items: [X]) -> (X?, Generator) ``` Chooses an input position uniformly. Empty input returns None without a draw. **Parameters** - `items` — values to choose from **Returns**: optional value and advanced state ### `sample` ```kex sample(items: [X], count: Integer) -> ([X], Generator) ``` Selects input positions without replacement, in random order. Duplicate input values may appear more than once in the result. A count larger than the input selects every position; zero consumes no draws. **Parameters** - `items` — population; unchanged by sampling - `count` — nonnegative number of positions to select **Returns**: selected values and advanced state **Examples** _Deal five cards reproducibly_ ```kex let (hand, advanced) = Random.seeded(42).sample(cards, count: 5) ``` ### `shuffle` ```kex shuffle(items: [X]) -> ([X], Generator) ``` Returns a random permutation, preserving duplicates and the input itself. Empty input consumes no draws. **Parameters** - `items` — values to shuffle **Returns**: permutation and advanced state --- --- package: prelude version: "0.4.0-rc.3-dev" source: range.kex title: Range entities: - { kind: type, name: "Range" } - { kind: make, name: "Range" } --- # Range ## type `Range` A span between two bounds, written `(1..10)` or `('a'..'z')`. A range stores its endpoints on both backends without building a list. Integer and character ranges can be enumerated with both ends included. Float ranges describe continuous bounds, for example `(-1.0..1.0)` for random sampling; `items` and traversal functions cannot enumerate them. A range whose lower endpoint exceeds its upper endpoint enumerates as an empty list. ```kex (1..5).items # => [1, 2, 3, 4, 5] (1..5).sum # => 15 5.in?(1..10) # => true ('a'..'e').items # => ['a', 'b', 'c', 'd', 'e'] ``` It is `Enumerable` and `Foldable`, so the traversal functions work directly, and the list operations below answer in list terms. Use `items` when you want a real list to hand to something else. ```kex (1..10).items.filter(~even?) # => [2, 4, 6, 8, 10] (1..3).items.each { |n| IO.printLine(n) } ``` Implements [`Enumerable`](enumerable.md#trait-enumerable), [`Foldable`](enumerable.md#trait-foldable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: A, f: (A -> B -> A)) -> A ``` Folds over the range's elements in ascending order. This is `Range`'s `Enumerable` primitive; the traversal functions are built on it. A range stays structurally minimal, so the fold runs over its materialized items. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each element **Returns**: the final accumulator **Examples** ```kex (1..4).reduce(0) { |sum, n| sum + n } # => 10 ``` #### `sum` Returns the sum of the range's elements. **Returns**: `Number` — the sum **Examples** ```kex (1..10).sum # => 55 (1..1).sum # => 1 ``` #### `product` Returns the product of the range's elements. **Returns**: `Number` — the product **Examples** ```kex (1..5).product # => 120 (five factorial) ``` #### `contains?` ```kex contains?(value: A) -> Bool ``` Returns `true` when `value` is one of the enumerated elements. This function materializes the range and supports integer and character ranges only. For continuous bounds, compare the value with each endpoint: `value >= lower && value <= upper`. **Parameters** - `value` — the value to test **Returns**: `true` when the value is in range **Examples** ```kex (1..10).contains?(5) # => true (1..10).contains?(11) # => false ``` #### `items` ```kex items : [A] ``` Returns the range's elements as a list. This is where a range stops being two numbers and becomes real data, so it is also where a very large range starts to cost something. **Returns**: the elements, in ascending order **Examples** ```kex (1..3).items # => [1, 2, 3] ('a'..'c').items # => ['a', 'b', 'c'] ``` _Building a lookup table_ ```kex (1..12).items.map { |m| (m, monthName(m)) } ``` #### `first` ```kex first : A? ``` Returns the first element, or `None` for an empty range. The list operations, over the materialized items. A range is an ordered collection, so `(1..5).first` and `(1..5).length` operate on its materialized elements. Floating bounds do not define such a collection. **Returns**: the first element, or `None` **Examples** ```kex (3..7).first # => Just(3) ``` #### `second` ```kex second : A? ``` Returns the second element, or `None` when the range is shorter than two. **Returns**: the second element, or `None` **Examples** ```kex (3..7).second # => Just(4) ``` #### `third` ```kex third : A? ``` Returns the third element, or `None` when the range is shorter than three. **Returns**: the third element, or `None` **Examples** ```kex (3..7).third # => Just(5) ``` #### `last` ```kex last : A? ``` Returns the last element, or `None` for an empty range. **Returns**: the last element, or `None` **Examples** ```kex (3..7).last # => Just(7) ``` #### `rest` ```kex rest : [A] ``` Returns every element after the first, as a list. **Returns**: the remaining elements **Examples** ```kex (1..4).rest # => [2, 3, 4] ``` #### `length` ```kex length : Integer ``` Returns how many elements the range has. **Returns**: the number of elements **Examples** ```kex (1..10).length # => 10 (5..5).length # => 1 ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns how many elements the range has. The same as `length`. **Returns**: the number of elements **Examples** ```kex (1..10).count # => 10 ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the range has no elements. **Returns**: `true` for an empty range **Examples** ```kex (1..3).empty? # => false ``` #### `max` ```kex max : A? ``` Returns the largest element, or `None` for an empty range. **Returns**: the largest element, or `None` **Examples** ```kex (1..10).max # => Just(10) ``` #### `min` ```kex min : A? ``` Returns the smallest element, or `None` for an empty range. **Returns**: the smallest element, or `None` **Examples** ```kex (1..10).min # => Just(1) ``` #### `reverse` ```kex reverse : [A] ``` Returns the elements as a list, in descending order. **Returns**: the elements, reversed **Examples** ```kex (1..4).reverse # => [4, 3, 2, 1] ``` _Counting down_ ```kex (1..3).reverse.each { |n| IO.printLine(n) } # prints 3, 2, 1 ``` #### `sort` ```kex sort : [A] ``` Returns the elements as a sorted list. A range is already ascending, so this is the identity: it exists so that generic code can call it. **Returns**: the elements, sorted **Examples** ```kex (1..4).sort # => [1, 2, 3, 4] ``` ```kex sort(comparator: (A -> A -> Bool)) -> [A] ``` Returns the elements as a list, ordered by `comparator`. **Parameters** - `comparator` — `true` when the first should come first **Returns**: the sorted elements **Examples** ```kex (1..4).sort { |a, b| a > b } # => [4, 3, 2, 1] ``` #### `uniq` ```kex uniq : [A] ``` Returns the elements as a list with duplicates removed. A range has none, so this is the identity: it exists so that generic code can call it. **Returns**: the distinct elements **Examples** ```kex (1..3).uniq # => [1, 2, 3] ``` #### `join` ```kex join(separator: String) -> String ``` Renders the elements as text, with `separator` between them. **Parameters** - `separator` — placed between elements **Returns**: the joined text **Examples** ```kex (1..4).join(", ") # => "1, 2, 3, 4" ``` _A comma-separated header_ ```kex "columns: ${(1..3).join(",")}" # => "columns: 1,2,3" ``` #### `at` ```kex at(index: Integer) -> A? ``` Returns the element at index `index`, counting from 0, or `None` when out of range. Note that the index counts positions, not values: `(10..20).at(0)` is `Just(10)`. **Parameters** - `index` — the 0-based position **Returns**: the element, or `None` **Examples** ```kex (10..20).at(0) # => Just(10) (10..20).at(99) # => None ``` #### `get` ```kex get(index: Integer) -> A? get(index: Integer, fallback: A) -> A ``` Returns the element at index `index`, counting from 0, or `None` when out of range. The same as `at`, named to match `List` and `Map`. **Parameters** - `index` — the 0-based position **Returns**: the element, or `None` **Examples** ```kex (10..20).get(0) # => Just(10) (10..20).get(99) # => None ``` #### `take` ```kex take(n: Integer) -> [A] ``` Returns the first `n` elements as a list. **Parameters** - `n` — how many elements to keep **Returns**: the leading elements **Examples** ```kex (1..10).take(3) # => [1, 2, 3] ``` #### `drop` ```kex drop(n: Integer) -> [A] ``` Returns everything after the first `n` elements, as a list. **Parameters** - `n` — how many elements to skip **Returns**: the remaining elements **Examples** ```kex (1..5).drop(3) # => [4, 5] ``` #### `indexOf` ```kex indexOf(value: A) -> Integer? ``` Returns the position of `value` in the range, or `None` when it is not in it. **Parameters** - `value` — the element to look for **Returns**: the 0-based position, or `None` **Examples** ```kex ('a'..'e').indexOf('c') # => Just(2) (10..20).indexOf(12) # => Just(2) ``` #### `zip` ```kex zip(other: [B]) -> [(A, B)] ``` Pairs each element with the element at the same position in `other`, stopping at the shorter of the two. **Parameters** - `other` — the list to pair with **Returns**: the pairs, in order **Examples** ```kex (1..3).zip(["a", "b", "c"]) # => [(1, "a"), (2, "b"), (3, "c")] ``` _Numbering a list_ ```kex (1..names.count).zip(names) ``` #### `partition` ```kex partition(pred: (A -> Bool)) -> ([A], [A]) ``` Splits the elements into those satisfying `pred` and those that do not. **Parameters** - `pred` — the test applied to each element **Returns**: the matching and non-matching elements **Examples** ```kex (1..4).partition { |n| n.even? } # => ([2, 4], [1, 3]) ``` #### `push` ```kex push(value: A) -> [A] ``` Returns the elements as a list with `value` added at the end. **Parameters** - `value` — the element to add **Returns**: the elements plus `value` **Examples** ```kex (1..3).push(4) # => [1, 2, 3, 4] ``` #### `reject` ```kex reject(pred: (A -> Bool)) -> [A] ``` Returns the elements that do NOT satisfy `pred`, as a list. **Parameters** - `pred` — the test applied to each element **Returns**: the elements that failed the predicate **Examples** ```kex (1..5).reject { |n| n.even? } # => [1, 3, 5] ``` ### From [`Enumerable`](enumerable.md#trait-enumerable) - [`map`](enumerable.md#enumerable-map) — Applies `f` to each item and collects the results into a list. - [`mapIndexed`](enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`filter`](enumerable.md#enumerable-filter) — Returns the items for which `pred` answers `true`. - [`flatMap`](enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](enumerable.md#trait-foldable) - [`each`](enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. --- --- package: prelude version: "0.4.0-rc.3-dev" source: regex.kex title: Regex entities: - { kind: module, name: "Regex" } --- # Regex ## module `Regex` ### `regex` ```kex regex(source: String) -> Result regex(parts: [String], values: [Any]) -> Regex ``` Compiles `source` into a `Regex`. Answers a `Result` because an arbitrary string may not be a valid pattern. Use this form when the pattern is built at run time: from a config file, from user input. For a pattern you write yourself, the tag form `` regex`\d+` `` is checked at compile time and hands back a bare `Regex`. **Parameters** - `source` — the pattern text **Returns**: the compiled pattern, or why it failed **Examples** ```kex regex("\\d+") # => Ok(Regex { source: "\\d+" }) regex("(") # => Error(RegexError { message: "missing closing parenthesis", ... }) ``` _Compiling a user-supplied filter_ ```kex match regex(pattern) do Ok(re) => lines.filter { |line| line.matches?(re) } Error(e) => do IO.printError("bad pattern at ${e.position}: ${e.message}") [] end end ``` ### `validateRegex` ```kex validateRegex(source: String) -> [TaggedValidation.Issue] ``` Validates a `regex` tagged literal at compile time. Found by the compiler through the standard `validate` naming convention (nothing about regex is special-cased). A raw `` regex`(` `` is a compile error, with the caret pointing at the byte offset PCRE2 reported inside the literal. Must stay pure: the compiler evaluates it on the tree-walk interpreter at compile time, so marking it `foul` would break builds, not just calls. **Parameters** - `source` — the literal's pattern text **Returns**: the problems found, if any ### `re` ```kex re(parts, values) -> Result ``` A short alias for `regex`, in both its forms. A second name bound to the same two functions, not a lexer synonym, so every property of `regex` (escaping, backend mapping, error shape) applies to it unchanged. Most code uses the tag form, where the shorter name reads better. **Parameters** - `source` — the pattern text (call form) **Returns**: the compiled pattern, or why it failed **Examples** ```kex re`https?://\S+` # tag form -> Regex re("\\d+") # call form -> Result ``` ### `validateRe` ```kex validateRe(source: String) -> [TaggedValidation.Issue] ``` Validates an `re` tagged literal at compile time, delegating to `validateRegex`. The compiler finds it by the same `validate` convention. **Parameters** - `source` — the literal's pattern text **Returns**: the problems found, if any ### `quote` ```kex quote(s: String) -> String ``` Escapes every regex metacharacter in `s`, so the result matches `s` literally. This is how to search for text that may itself contain pattern syntax: a search term typed by a user, a filename, a version string. It escapes per character rather than wrapping in `\Q...\E`, which a value containing `\E` would break out of. **Parameters** - `s` — the text to escape **Returns**: a pattern matching `s` literally **Examples** ```kex Regex.quote("a.b") # => "a\\.b" ``` _Searching for a literal term supplied by a user_ ```kex match regex(Regex.quote(term)) do Ok(needle) => lines.filter { |line| line.matches?(needle) } Error(_) => [] end ``` ### `matches` ```kex matches(s: String, re: Regex) -> Match? ``` Finds the first occurrence of `re` in `s` and returns what it matched. Unanchored: the pattern may match anywhere in the string. Anchor it with `^...$` when a whole-string match is what you mean. Answers `None` when the pattern does not occur. **Parameters** - `s` — the text to search - `re` — the pattern to look for **Returns**: the first match, or `None` **Examples** ```kex "order #4271".matches(re`#(\d+)`).flatMap { |m| m.get(1) } # => Just("4271") "no number here".matches(re`#(\d+)`) # => None ``` _Pulling a version out of a line_ ```kex line.matches(re`v(\d+)\.(\d+)`).map { |m| (m.get(1, "0"), m.get(2, "0")) } ``` ### `matches?` ```kex matches?(s: String, re: Regex) -> Bool ``` Returns `true` when `re` occurs anywhere in `s`. The boolean form of `matches`, and unanchored for the same reason. Use it when you only need to know whether the pattern is there. **Parameters** - `s` — the text to search - `re` — the pattern to look for **Returns**: `true` when the pattern occurs **Examples** ```kex "hello".matches?(re`^h`) # => true "hello".matches?(re`\d`) # => false ``` _Keeping only the lines that look like assignments_ ```kex lines.filter { |line| line.matches?(re`^\w+\s*=`) } ``` ### `scan` ```kex scan(s: String, re: Regex) -> [Match] ``` Returns every match of `re` in `s`, left to right. Always answers `[Match]`, whether or not the pattern has capture groups: adding a group to a pattern must not change the type flowing out of `scan`. An empty list means nothing matched. **Parameters** - `s` — the text to search - `re` — the pattern to look for **Returns**: every match, in order **Examples** ```kex "cat=1, dog=2".scan(re`(\w+)=(\d+)`).map { |m| m.get(1, "") } # => ["cat", "dog"] ``` _Building a map out of key=value pairs_ ```kex Kex.Intrinsic.Map.fromEntries( text.scan(re`(\w+)=(\w+)`).map { |m| (m.get(1, ""), m.get(2, "")) } ) ``` ### `replace` ```kex replace : String -> Regex -> String | (Match) -> String -> String ``` Replaces EVERY match of `re` in `s`: this is `gsub`, not `sub`. The replacement is either a literal `String`, inserted verbatim with no `$1` / `\1` backreference syntax, or a block receiving the `Match`, which is how a replacement built from what was captured is written. **Parameters** - `s` — the text to rewrite - `re` — the pattern to replace - `replacement` — the literal text, or a block **Returns**: the rewritten text **Examples** _A literal replacement_ ```kex "a-b-c".replace(re`-`, "+") # => "a+b+c" ``` _A replacement computed from the match_ ```kex "2026-7-4".replace(re`(\d+)`) do |m| m.get(1, "").length.to(String).or("") end # => "4-1-1" ``` _Redacting numbers in a log line_ ```kex line.replace(re`\d`, "x") ``` ### `splitLimit` ```kex splitLimit : String -> Regex -> Integer -> [String] ``` Splits `s` on `re`, capping the number of fields. A positive `limit` caps the field count, leaving the remainder unsplit in the last field; a negative `limit` keeps trailing empty fields instead of dropping them. Plain `s.split(re)` needs no function here at all: it resolves to `String.split`, which dispatches to this engine when handed a Regex (in both backends), and follows Ruby's semantics: trailing empty fields are dropped, leading ones are kept, and capture groups are interleaved into the result. Use `Regex.splitWithLimit` to control the number of fields. **Parameters** - `s` — the text to split - `re` — the separator pattern - `limit` — the field cap; negative keeps trailing empty fields **Returns**: the fields **Examples** _Plain splitting needs no limit_ ```kex "a, b ,c".split(re`\s*,\s*`) # => ["a", "b", "c"] ``` _Splitting off just the first field_ ```kex Regex.splitLimit("a,b,c", re`,`, 2) # => ["a", "b,c"] ``` _Keeping the trailing empty fields_ ```kex Regex.splitLimit("a,b,,", re`,`, -1) # => ["a", "b", "", ""] ``` ## record `Regex` Regular expressions, backed by PCRE2 in the interpreter and Erlang's `re` on BEAM: the same PCRE pattern language on both. Opt-in, not prelude: nothing here is in scope until `using Regex`. There are two ways to write a pattern. The tagged literal is the everyday one: it is checked when your program is compiled, so it cannot fail at run time and gives you a bare `Regex`. ```kex using Regex main do let line = "order #4271 shipped" IO.printLine(line.matches?(re`#\d+`)) # => true IO.printLine(line.matches(re`#(\d+)`).flatMap { |m| m.get(1) }.or("")) # => 4271 end ``` The call form takes a pattern built at run time and answers a `Result`, because an arbitrary string may not be a valid pattern: ```kex match regex(userSupplied) do Ok(pattern) => IO.printLine(text.matches?(pattern)) Error(e) => IO.printError("bad pattern at ${e.position}: ${e.message}") end ``` The operations are `matches?` (is it there), `matches` (find the first), `scan` (find them all), `replace`, and `split`. A compiled regular expression. Create one with a `re` tagged literal or `regex(source)`. ```kex let digits = re`[0-9]+` "item 42".matches(digits).flatMap { |m| m.get(0) } # => Just("42") ``` **Fields** - `source` : [String](string.md#make-string) ## record `RegexError` A pattern that failed to compile. Mirrors `ParseError`: the `position` is a *character* offset into the pattern, not a byte offset, so it agrees across backends on patterns containing non-ASCII. ```kex regex("(") # => Error(RegexError { source: "(", position: 1, # message: "missing closing parenthesis" }) ``` **Fields** - `source` : [String](string.md#make-string) - `position` : [Integer](number.md#make-integer) - `message` : [String](string.md#make-string) ## record `Match` A successful match, and the groups it captured. It behaves like a map keyed by both group number (`0` is the whole match) and group name (`:year`), but is a named type so it can grow spans and surrounding context later without breaking existing `get` call sites. A group that did not participate is an absent key, so `get` answers `None`. This differs from a group that matched the empty string, which answers `Just("")`. **Fields** - `captures` : [Map](map.md#type-map) ### Functions #### `get` ```kex get(key: Atom | Integer) -> String? get(key: Atom | Integer, default: String) -> String ``` Returns the text captured by a group, by number or by name. Group `0` is the whole match; `1` is the first parenthesised group. A named group `(?...)` is reached with the atom `:year`. A group that did not participate answers `None`. **Parameters** - `key` — the group number or name **Returns**: the captured text, or `None` **Examples** ```kex let found = "order #4271".matches(re`#(\d+)`) found.flatMap { |m| m.get(0) } # => Just("#4271") found.flatMap { |m| m.get(1) } # => Just("4271") ``` _Named groups_ ```kex "2026-07-04".matches(re`(?\d{4})-(?\d{2})`) .flatMap { |m| m.get(:year) } # => Just("2026") ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: stream.kex title: Stream entities: - { kind: type, name: "Stream" } - { kind: module, name: "Stream" } - { kind: make, name: "Stream" } --- # Stream ## type `Stream` A lazy, potentially infinite sequence. A stream describes how to produce its elements rather than holding them, so an infinite one is an ordinary value. Nothing is computed until you ask for elements with `take`. ```kex let naturals = Stream.Sequence(from: 0) { |n| n + 1 } naturals.take(5) # => [0, 1, 2, 3, 4] naturals.map { |n| n * n }.take(4) # => [0, 1, 4, 9] naturals.filter { |n| n.even? }.take(3) # => [0, 2, 4] ``` `map`, `filter` and `drop` all answer with another stream, so a pipeline stays lazy end to end; `take` is what turns it into a list. Streams are best for generated sequences you may revisit. A file or socket is different: it can only be consumed once, so those APIs return a `Feed`. Convert a small feed with `toStream` only when replaying it is worth keeping every value already read. ### Functions #### `take` ```kex take(n: Integer) -> [A] ``` Returns the first `n` elements as a list, computing the stream up to that point. This is the operation that ends a lazy pipeline and gives you real data. **Parameters** - `n` — how many elements to produce **Returns**: the first `n` elements **Examples** ```kex Stream.Sequence(from: 1) { |n| n + 1 }.take(3) # => [1, 2, 3] ``` _The first ten squares_ ```kex Stream.Sequence(from: 1) { |n| n + 1 } .map { |n| n * n } .take(10) ``` _Generating retry delays without building an unbounded list_ ```kex let delays = Stream.Sequence(from: 1.seconds) { |d| d * 2 } delays.take(4) # => [1 second, 2 seconds, 4 seconds, 8 seconds] ``` #### `drop` ```kex drop(n: Integer) -> Stream ``` Returns a new stream that skips the first `n` elements. Still a stream, so the result stays lazy: pair it with `take` to get a window out of the middle. **Parameters** - `n` — how many elements to skip **Returns**: the stream, offset by `n` **Examples** ```kex Stream.Sequence(from: 0) { |n| n + 1 }.drop(3).take(3) # => [3, 4, 5] ``` _Paging through a generated sequence_ ```kex let page(n: Integer) = source.drop(n * 20).take(20) ``` #### `map` ```kex map(f: (A -> B)) -> Stream ``` Returns a new stream with `f` applied to each element. `f` is not called until elements are taken, and then only for those that are. **Parameters** - `f` — applied to each element **Returns**: the mapped stream **Examples** ```kex Stream.Sequence(from: 1) { |n| n + 1 }.map { |n| n * n }.take(4) # => [1, 4, 9, 16] ``` _Formatting as it goes_ ```kex Stream.Sequence(from: 1) { |n| n + 1 } .map { |n| "item ${n}" } .take(3) # => ["item 1", "item 2", "item 3"] ``` #### `filter` ```kex filter(pred: (A -> Bool)) -> Stream ``` Returns a new stream with only the elements `pred` accepts. Producing `n` filtered elements may require walking many more upstream ones, so a predicate that almost never holds makes `take` run for a long time, and one that never holds makes it run forever. **Parameters** - `pred` — the test applied to each element **Returns**: the filtered stream **Examples** ```kex let evens = Stream.Sequence(from: 0) { |n| n + 1 }.filter { |n| n.even? } evens.take(4) # => [0, 2, 4, 6] ``` _Multiples of three, formatted_ ```kex Stream.Sequence(from: 1) { |n| n + 1 } .filter { |n| n.modulo(3) == 0 } .map { |n| "#${n}" } .take(3) # => ["#3", "#6", "#9"] ``` #### `each` ```kex each(f: (A -> Void)) -> Void ``` Applies `f` to every element. Only ever finishes on a stream that ends: a file's lines converted with `Feed.toStream`, or anything `take` has bounded. On `Stream.Sequence` this runs forever, exactly as writing the same loop by hand would. **Parameters** - `f` — applied to each element **Examples** ```kex Stream.Sequence(from: 1) { |n| n + 1 } .map { |n| n * n } .toFeed .take(3) .each { |n| IO.printLine(n) } ``` #### `toFeed` ```kex toFeed : Feed ``` Returns a `Feed` that walks this stream once. A stream remembers every element forced through it, so walking a long one holds all of it for as long as the stream is in scope. A feed drawn from it walks without holding the start, letting each element go as it passes: the way to consume a long stream in constant space. **Returns**: a one-shot cursor over the stream **Examples** _Summing a long generated sequence without holding it_ ```kex let total = Stream.Sequence(from: 1) { |n| n + 1 } .toFeed .take(1000000) .sum ``` ## module `Stream` Constructors for `Stream`. ### `Sequence` ```kex Sequence(from: A, step: (A -> A)) -> Stream ``` Builds an infinite stream from a first element and a step function. The stream is `from`, then `step(from)`, then `step(step(from))`, and so on: nothing is computed until you take from it. **Parameters** - `from` — the first element - `step` — produces the next element from the current one **Returns**: the generated stream **Examples** _Counting up_ ```kex let naturals = Stream.Sequence(from: 0) { |n| n + 1 } naturals.take(5) # => [0, 1, 2, 3, 4] ``` _Powers of two_ ```kex let powers = Stream.Sequence(from: 1) { |n| n * 2 } powers.take(6) # => [1, 2, 4, 8, 16, 32] ``` _A geometric decay_ ```kex Stream.Sequence(from: 1.0) { |x| x / 2.0 }.take(4) # => [1.0, 0.5, 0.25, 0.125] ``` ### `Iterate` ```kex Iterate(seed: A, step: (A -> A)) -> Stream ``` Builds an infinite stream from a seed and a step function. The same thing as `Sequence`: use whichever reads better where you are. **Parameters** - `seed` — the first element - `step` — produces the next element from the current one **Returns**: the generated stream **Examples** ```kex let odds = Stream.Iterate(1) { |n| n + 2 } odds.take(5) # => [1, 3, 5, 7, 9] ``` ### `empty` (constant) The stream with no elements. **Examples** ```kex Stream.empty.take(3) # => [] ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: string.kex title: String entities: - { kind: make, name: "String" } - { kind: make, name: "Char" } - { kind: module, name: "String" } - { kind: make, name: "Tuple" } --- # String ## type `String` Implements [`Monoid`](algebra.md#trait-monoid), [`Blankable`](blankable.md#trait-blankable), [`Comparable`](comparable.md#trait-comparable), [`Enumerable`](enumerable.md#trait-enumerable), [`Foldable`](enumerable.md#trait-foldable), [`Truthyable`](truthyable.md#trait-truthyable). Text. A `String` is a sequence of Unicode characters, immutable like every other Kex value. Operations that transform text return a new string rather than changing the receiver. A `String` is its own type, not a list of characters. It is `Enumerable`, so `each`, `reduce`, `find` and friends walk it one `Char` at a time, and the sequence operations that could reasonably answer in either currency pick the useful one: `take`, `drop` and `sort` hand back a `String`, while `first` and `last` hand back a `Char`. Use `chars` to cross over to a real list. ```kex let line = " Hello, World " line.trim.lowerCase.split(", ") # => ["hello", "world"] line.trim.take(5) # => "Hello" line.trim.chars.count(~upper?) # => 2 ``` Strings interpolate with `${...}`: ```kex let name = "Ada" "hello, ${name}" # => "hello, Ada" ``` ### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: A, f: (A -> Char -> A)) -> A ``` Folds the string from the left, one `Char` at a time. This is `String`'s `Enumerable` primitive: `map`, `filter`, `find`, `any?` and the rest are defined in terms of it. Reach for it directly when you are accumulating something that is neither a string nor a list. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each character **Returns**: the final accumulator **Examples** _Summing digit values_ ```kex "12345".reduce(0) { |sum, c| sum + c.codepoint - 48 } # => 15 ``` _Building a character histogram_ ```kex "banana".reduce({}) do |counts, c| counts.put(c.string, counts.get(c.string).or(0) + 1) end # => {"a": 3, "b": 1, "n": 2} ``` ### `mapChars` ```kex mapChars(f: (Char -> Char)) -> String ``` Applies `f` to every character and joins the results back into a `String`. This is the string-preserving counterpart of `map`. `map` comes from `Enumerable` and collects into a list, because its type says `(Char -> B) -> [B]` and `B` need not be a `Char` at all. When you want a string out, name the operation that produces one. **Parameters** - `f` — applied to each character **Returns**: the mapped characters, as a string **Examples** ```kex "hi".mapChars(&.upperCase) # => "HI" "hi".map(&.upperCase) # => ['H', 'I'] (a list, per Enumerable) ``` _Shifting every character up one codepoint_ ```kex "abc".mapChars do |c| String.fromCodepoint(c.codepoint + 1).or("").first.or(c) end # => "bcd" ``` ### `filter` (from Enumerable) ```kex filter(pred: (Char -> Bool)) -> String ``` Returns the characters satisfying `pred`, as a `String`. **Parameters** - `pred` — kept when it answers `true` **Returns**: the matching characters, in order **Examples** _Keeping only the digits_ ```kex "a1b2c3".filter(~digit?) # => "123" ``` _Stripping punctuation before comparing_ ```kex "Hello, World!".filter(~alpha?).lowerCase # => "helloworld" ``` ### `uniq` ```kex uniq : String ``` Returns the string with repeated characters removed, keeping the first occurrence of each. **Returns**: the distinct characters, in first-seen order **Examples** ```kex "banana".uniq # => "ban" "hello".uniq # => "helo" ``` ### `first` ```kex first : Char? ``` A String is its OWN type, not a [Char]: `chars` converts between them. So the sequence operations below are declared here rather than inherited from List, and they answer in String's terms: `take`/`drop`/`sort` hand back a String, while `first`/`last` hand back a Char. The List intrinsics they delegate to already understand the String representation, so there is one implementation for both backends. Returns the first character, or `None` when the string is empty. **Returns**: the first character, or `None` **Examples** ```kex "hi".first # => Just('h') "".first # => None ``` _Testing an initial without risking an empty string_ ```kex "hello".first.map(~upper?).or(false) # => false ``` ### `last` ```kex last : Char? ``` Returns the last character, or `None` when the string is empty. **Returns**: the last character, or `None` **Examples** ```kex "hi".last # => Just('i') "".last # => None ``` ### `get` ```kex get(i: Integer) -> Char? get(i: Integer, default: Char) -> Char ``` Returns the character at index `i`, counting from 0. Answers `None` for an index past either end rather than failing, so it is safe to index with a computed position. **Parameters** - `i` — the 0-based index **Returns**: the character, or `None` when out of range **Examples** ```kex "hi".get(1) # => Just('i') "hi".get(9) # => None ``` ### `take` ```kex take(n: Integer) -> String ``` Returns the first `n` characters. A short string is returned whole, so `take` never fails on an `n` that is too large. **Parameters** - `n` — how many characters to keep **Returns**: the leading `n` characters **Examples** ```kex "hello".take(2) # => "he" "hello".take(99) # => "hello" "hello".take(0) # => "" ``` _Truncating for display_ ```kex let preview = title.take(30) + (title.count > 30 then "…" else "") ``` ### `drop` ```kex drop(n: Integer) -> String ``` Returns everything after the first `n` characters. The complement of `take`: +s.take(n) + s.drop(n)+ is `s`. **Parameters** - `n` — how many characters to skip **Returns**: the remaining characters **Examples** ```kex "hello".drop(2) # => "llo" "hello".drop(99) # => "" ``` _Removing a known prefix_ ```kex let flag = "--verbose" flag.startsWith?("--") then flag.drop(2) else flag # => "verbose" ``` ### `reject` ```kex reject(pred: (Char -> Bool)) -> String ``` Returns the characters that do NOT satisfy `pred`: the complement of `filter`. **Parameters** - `pred` — dropped when it answers `true` **Returns**: the characters that failed the predicate **Examples** ```kex "hello".reject { |c| c == 'l' } # => "heo" ``` _Removing whitespace_ ```kex "1 234 567".reject(~space?) # => "1234567" ``` ### `sort` ```kex sort : String ``` Returns the characters in ascending codepoint order. **Returns**: the sorted characters **Examples** ```kex "hello".sort # => "ehllo" ``` _An anagram check_ ```kex "listen".sort == "silent".sort # => true ``` ### `indexOf` ```kex indexOf(c: Char) -> Integer? ``` Returns the index of the first occurrence of `c`, or `None` when the character does not appear. **Parameters** - `c` — the character to look for **Returns**: the 0-based index, or `None` **Examples** ```kex "hello".indexOf('l') # => Just(2) "hello".indexOf('z') # => None ``` _Splitting a +key=value+ pair at the first +=+_ ```kex let pair = "host=localhost" pair.indexOf('=').map { |i| (pair.take(i), pair.drop(i + 1)) } # => Just(("host", "localhost")) ``` ### `findIndex` ```kex findIndex(pred: (Char -> Bool)) -> Integer? ``` Returns the index of the first character satisfying `pred`, or `None`. The predicate counterpart of `indexOf`, which searches for one known character. **Parameters** - `pred` — the test applied to each character **Returns**: the 0-based index, or `None` **Examples** ```kex "hello".findIndex { |c| c == 'l' } # => Just(2) "hello".findIndex(~digit?) # => None ``` _Finding where the leading indentation ends_ ```kex " text".findIndex { |c| !c.space? } # => Just(4) ``` ### `zip` ```kex zip(other: [Y]) -> [(Char, Y)] ``` Pairs each character with the element at the same index in `other`, stopping at the shorter of the two. **Parameters** - `other` — the list to pair with **Returns**: the pairs, in order **Examples** ```kex "ab".zip([1, 2]) # => [('a', 1), ('b', 2)] "abc".zip([1, 2]) # => [('a', 1), ('b', 2)] ``` _Numbering the characters_ ```kex "abc".zip((0..2).items) # => [('a', 0), ('b', 1), ('c', 2)] ``` ### `partition` ```kex partition(pred: (Char -> Bool)) -> (String, String) ``` Splits the string in two: the characters satisfying `pred`, then those that do not. One pass, both answers. **Parameters** - `pred` — the test applied to each character **Returns**: the matching and non-matching characters **Examples** ```kex "hello".partition { |c| c == 'l' } # => ("ll", "heo") ``` _Separating digits from the rest_ ```kex let (digits, other) = "a1b2".partition(~digit?) digits # => "12" other # => "ab" ``` ### `count` (from Foldable) ```kex count : Integer ``` Returns the number of characters: characters, not bytes. See `bytes` for the storage-level length. **Returns**: the character count **Examples** ```kex "hello".count # => 5 "".count # => 0 "é".count # => 1 (but "é".bytes.count is 2) ``` ### `length` ```kex length : Integer ``` Returns the number of characters. The same as `count`, under the name familiar from other languages. **Returns**: the character count **Examples** ```kex "hello".length # => 5 ``` ### `graphemeCount` ```kex graphemeCount : Integer ``` Returns the number of user-perceived characters: extended grapheme clusters (Unicode UAX #29), which is not always `count`. `count` answers the codepoint count, and most text is one codepoint per grapheme cluster, so the two usually agree. They part ways for a base character combined with a following mark, an emoji built from more than one codepoint, and `"\r\n"`, which is one grapheme cluster over two codepoints: reach for `graphemeCount` over `count` wherever "how many characters does a person see" is the question, such as sizing text for display or truncating it at a boundary a reader would recognize. **Returns**: the grapheme cluster count **Examples** ```kex "hello".graphemeCount # => 5, same as count "\r\n".graphemeCount # => 1 ("\r\n".count is 2: a CR codepoint and an LF codepoint) ``` _A base character plus a combining mark_ ```kex let acute = String.fromCodepoint(0x301).or("") # combining acute accent let e = "e" + acute e.count # => 2 (two codepoints: 'e' and the combining mark) e.graphemeCount # => 1 (one character on screen) ``` ### `empty?` ```kex empty? : Bool ``` Returns `true` when the string has no characters. Note that a string of spaces is not empty: use `blank?` from the `Blankable` trait when whitespace should not count. **Returns**: `true` for the empty string **Examples** ```kex "".empty? # => true "hi".empty? # => false " ".empty? # => false ``` ### `enclose` ```kex enclose(wrapper: String) -> String enclose(left: String, right: String) -> String ``` Returns the string with `wrapper` added at both ends. **Parameters** - `wrapper` — placed before and after the string **Returns**: the wrapped string **Examples** ```kex "hello".enclose("*") # => "*hello*" "hello".enclose("__") # => "__hello__" ``` _Quoting a value for output_ ```kex value.enclose("\"") # => "\"localhost\"" ``` ### `at` ```kex at(i: Integer) -> Char? ``` Returns the character at index `i`, counting from 0, or `None` when the index is out of range. The same as the one-argument `get`. **Parameters** - `i` — the 0-based index **Returns**: the character, or `None` **Examples** ```kex "hello".at(1) # => Just('e') "hello".at(9) # => None ``` ### `second` ```kex second : Char? ``` Returns the second character, or `None` when the string is shorter than two characters. **Returns**: the second character, or `None` **Examples** ```kex "hi".second # => Just('i') "h".second # => None ``` ### `third` ```kex third : Char? ``` Returns the third character, or `None` when the string is shorter than three characters. **Returns**: the third character, or `None` **Examples** ```kex "hip".third # => Just('p') "hi".third # => None ``` ### `rest` ```kex rest : String ``` Returns everything after the first character. The empty string has no rest, and answers with itself. **Returns**: the string without its first character **Examples** ```kex "hello".rest # => "ello" "h".rest # => "" ``` ### `chars` ```kex chars : [Char] ``` Returns the string's characters as a list. The bridge from `String` to the `List` functions that a string does not have of its own. Join back with `.join("")`. **Returns**: the characters, in order **Examples** ```kex "hi".chars # => ['h', 'i'] ``` _Going out to List and back_ ```kex "hello".chars.reverse.join("") # => "olleh" ``` ### `bytes` ```kex bytes : [Byte] ``` Returns the string's UTF-8 encoding, one `Byte` per byte. `chars` is the TEXT view of a string and `bytes` is the STORAGE view, so they differ for anything outside ASCII: `"é"` is one `Char` and two +Byte+s. Use `bytes` when the length that matters is the encoded one: a network frame, a file offset, a size limit. `String.fromBytes` is the inverse. **Returns**: the UTF-8 bytes **Examples** ```kex "hi".bytes # => [104, 105] "é".bytes # => [195, 169] "é".chars # => ['é'] ``` _Measuring the encoded size_ ```kex "héllo".count # => 5 "héllo".bytes.count # => 6 ``` ### `byteSize` ```kex byteSize : Integer ``` The storage size in bytes, without building the byte list. `bytes.count` answers the same number by materialising every byte first, which on a megabyte payload means a million values to count them. This reads the encoded length directly. **Returns**: the length of the UTF-8 encoding **Examples** _Text length and storage length differ_ ```kex "héllo".count # => 5 "héllo".byteSize # => 6 ``` ### `byteAt` ```kex byteAt(index: Integer) -> Byte? ``` One byte of the storage view, or `None` when the index is out of range. This indexes the ENCODING, not the text: `byteAt` on a multi-byte character returns one of its bytes, never the character. Use `at` or `chars` for the text view. **Parameters** - `index` — a zero-based byte offset **Returns**: the byte, or `None` past the end **Examples** _The two views of the same string_ ```kex "héllo".byteAt(1) # => Just(195) "héllo".at(1) # => Just("é") "héllo".byteAt(99) # => None ``` ### `bytePart` ```kex bytePart(offset: Integer, count: Integer) -> String ``` A slice of the storage view, by byte offset and byte count. The range is clamped rather than refused, the way `take` and `drop` already behave. Slicing mid-character yields a string holding partial UTF-8 — legal storage, but not text: decode it only at a boundary you know is a character boundary. **Parameters** - `offset` — a zero-based byte offset, clamped to the string - `count` — how many bytes, clamped to what remains **Returns**: the bytes in that range **Examples** _Reading a length-prefixed field out of a payload_ ```kex payload.bytePart(4, payload.byteSize - 4) ``` ### `split` ```kex split : [String] ``` Splits the string into its individual characters, as one-character strings. Use `chars` instead when you want `Char` values rather than strings. **Returns**: one string per character **Examples** ```kex "hi".split # => ["h", "i"] ``` ```kex split(sep: String | Regex) -> [String] split : [String] ``` Splits the string on every occurrence of `sep`, which may be a literal string or a `Regex`. Separators at the ends produce empty parts, so splitting `",a,"` on `","` gives three parts. Filter or trim afterwards when that is not wanted. **Parameters** - `sep` — the separator to split on **Returns**: the parts, in order **Examples** _Splitting a delimited line_ ```kex "a,b,c".split(",") # => ["a", "b", "c"] "a, b, c".split(", ") # => ["a", "b", "c"] ``` _Splitting on a pattern (needs `using Regex`)_ ```kex "a1b22c".split(re`[0-9]+`) # => ["a", "b", "c"] ``` _Empty parts at the edges are kept_ ```kex ",a,".split(",") # => ["", "a", ""] ``` ### `lines` ```kex lines : [String] ``` Splits the string into lines on `\n`. A single trailing newline is dropped, so a file that ends in one does not produce a final empty line. Windows line endings are not stripped: split on `\r\n` explicitly, or `trim` each line, when that matters. **Returns**: the lines, without their newlines **Examples** ```kex "a\nb".lines # => ["a", "b"] "a\nb\n".lines # => ["a", "b"] "".lines # => [] ``` _Numbering the lines of a file_ ```kex text.lines.zip((1..text.lines.count).items).each do |pair| let (line, n) = pair IO.printLine("${n}: ${line}") end ``` ### `indentRest` ```kex indentRest(prefix: String) -> String ``` Indents every line but the first by `prefix`. This is what splicing a multi-line value into an indented `${...}` hole needs: the hole's own indentation already covers line one, and the rest have to catch up. Blank lines stay blank, and (through `lines`) a single trailing newline is dropped, so a block does not push a stray empty line into its slot. **Parameters** - `prefix` — the indentation to add **Returns**: the re-indented string **Examples** ```kex "a\nb\n".indentRest(" ") # => "a\n b" "a".indentRest(" ") # => "a" ``` _Splicing a block into a template_ ```kex let body = "one\ntwo" "items:\n ${body.indentRest(" ")}" # => "items:\n one\n two" ``` ### `replace` ```kex replace(pattern: String, replacement: String) -> String ``` Replaces every literal occurrence of `pattern` with `replacement`. The pattern is matched literally, not as a regular expression. An empty pattern matches at every character boundary, including both ends. **Parameters** - `pattern` — the text to look for - `replacement` — what to put in its place **Returns**: the rewritten string **Examples** ```kex "a-b-c".replace("-", "+") # => "a+b+c" "abc".replace("", "-") # => "-a-b-c-" ``` _Normalising a path separator_ ```kex "a\\b\\c".replace("\\", "/") # => "a/b/c" ``` ### `substitute` ```kex substitute(replacements: {String: String}) -> String ``` Replaces every key of `replacements` with its value, in one pass over the map. The keys are the placeholders exactly as written: no syntax is imposed and nothing is reserved, so `$NAME$`, `__NAME__`, `{{name}}` and `%name%` are all equally valid, and a template needs no escaping to be a template. `$NAME$` is the convention to reach for when there is no reason to prefer another. Note that a placeholder is NOT `${name}`: that is interpolation, which the compiler resolves before this ever sees the string. This is what a template wants instead of a chain of `replace` calls: the chain reads as a pipeline when it is really a substitution table, and it quietly depends on its own order, since an earlier replacement's OUTPUT is still visible to every later one. Substitutions are applied in the map's canonical key order and each is applied to the result of the last, so a value that itself contains a key can still be rewritten by a later one. Keep values placeholder-free when that matters. **Parameters** - `replacements` — placeholder to replacement text **Returns**: the filled-in template **Examples** ```kex "Hello, $WHO$!".substitute({"$WHO$": "world"}) # => "Hello, world!" "$A$ and $B$".substitute({"$A$": "x", "$B$": "y"}) # => "x and y" "__A__".substitute({"__A__": "any key works"}) # => "any key works" ``` _A reusable template_ ```kex let greeting = "Dear $NAME$,\n\nYour order $ID$ has shipped." greeting.substitute({"$NAME$": "Ada", "$ID$": "A-1701"}) ``` ### `trim` ```kex trim : String ``` Removes leading and trailing whitespace. Whitespace inside the string is left alone. **Returns**: the trimmed string **Examples** ```kex " hello ".trim # => "hello" "\n hi \t".trim # => "hi" "a b".trim # => "a b" ``` _Cleaning up user input_ ```kex let name = IO.getLine.or("").trim ``` ### `upperCase` ```kex upperCase : String ``` Returns the string with every character converted to upper case. **Returns**: the upper-cased string **Examples** ```kex "hello".upperCase # => "HELLO" "Hello, ada".upperCase # => "HELLO, ADA" ``` _Case-insensitive comparison_ ```kex input.lowerCase == "yes" ``` ### `lowerCase` ```kex lowerCase : String ``` Returns the string with every character converted to lower case. **Returns**: the lower-cased string **Examples** ```kex "HELLO".lowerCase # => "hello" ``` _Normalising a lookup key_ ```kex settings.get(key.trim.lowerCase) ``` ### `capitalize` ```kex capitalize : String ``` Returns the string with its first character upper-cased and the rest LOWER-cased. This matches Ruby's `String#capitalize`, so `"hELLO"` becomes `"Hello"`, not `"HELLO"`. Use `upperCase` when the whole string should shout. It is also what building a type name from data needs: a generated `type %name = ...` inside a `compiled do` block requires an upper-case identifier, so a list of lower-case words has to be capitalized first. **Returns**: the capitalized string **Examples** ```kex "hello".capitalize # => "Hello" "hELLO".capitalize # => "Hello" (tail is lower-cased) "already Capital".capitalize # => "Already capital" "".capitalize # => "" ``` _Title-casing a sentence_ ```kex "the quick brown fox".split(" ").map(~capitalize).join(" ") # => "The Quick Brown Fox" ``` ### `snakeCase` ```kex snakeCase : String ``` Returns the identifier in `snake_case`: its words lower-cased and joined with underscores. Words are separated by `_`, `-` and spaces, and by case: a capital after a lower-case letter or digit starts a word (`userId`), and so does the last capital of a run followed by a lower-case letter (`HTTPServer` is `HTTP`, `Server`). Anything else stays in its word, so `empty?` keeps its `?`. **Returns**: the snake-cased identifier **Examples** ```kex "userId".snakeCase # => "user_id" "HTTPServer".snakeCase # => "http_server" "show-nickname".snakeCase # => "show_nickname" ``` ### `kebabCase` ```kex kebabCase : String ``` Returns the identifier in `kebab-case`: its words lower-cased and joined with hyphens. Words are found as `snakeCase` finds them. **Returns**: the kebab-cased identifier **Examples** ```kex "userId".kebabCase # => "user-id" "show_nickname".kebabCase # => "show-nickname" ``` ### `camelCase` ```kex camelCase : String ``` Returns the identifier in `camelCase`: its first word lower-cased and the rest capitalized, with nothing between them. Words are found as `snakeCase` finds them. **Returns**: the camel-cased identifier **Examples** ```kex "show_nickname".camelCase # => "showNickname" "HTTP server".camelCase # => "httpServer" ``` ### `pascalCase` ```kex pascalCase : String ``` Returns the identifier in `PascalCase`: every word capitalized, with nothing between them. Words are found as `snakeCase` finds them. **Returns**: the Pascal-cased identifier **Examples** ```kex "show_nickname".pascalCase # => "ShowNickname" "user-id".pascalCase # => "UserId" ``` ### `identifierWords` ```kex identifierWords : [Char] -> Char? -> String -> [String] -> [String] ``` The words of an identifier, read one character at a time: `previous` is the character before `chars`, `current` the word being read. ### `reverse` ```kex reverse : String ``` Returns the string with its characters in reverse order. **Returns**: the reversed string **Examples** ```kex "hello".reverse # => "olleh" ``` _A palindrome check_ ```kex let cleaned = "A man, a plan, a canal: Panama".filter(~alpha?).lowerCase cleaned == cleaned.reverse # => true ``` ### `contains?` ```kex contains?(sub: String) -> Bool ``` Returns `true` when `sub` appears anywhere in the string. The search is literal and case-sensitive. Lower-case both sides to ignore case; use `Regex` when the needle is a pattern. **Parameters** - `sub` — the text to look for **Returns**: `true` when it is present **Examples** ```kex "hello world".contains?("world") # => true "hello world".contains?("xyz") # => false "hello world".contains?("World") # => false ``` _Ignoring case_ ```kex "Hello".lowerCase.contains?("hello") # => true ``` ### `startsWith?` ```kex startsWith?(prefix: String) -> Bool ``` Returns `true` when the string begins with `prefix`. **Parameters** - `prefix` — the expected opening text **Returns**: `true` when the string starts with it **Examples** ```kex "hello".startsWith?("hel") # => true "hello".startsWith?("llo") # => false ``` _Recognising a command-line flag_ ```kex args.filter { |a| a.startsWith?("--") } ``` ### `endsWith?` ```kex endsWith?(suffix: String) -> Bool ``` Returns `true` when the string ends with `suffix`. **Parameters** - `suffix` — the expected closing text **Returns**: `true` when the string ends with it **Examples** ```kex "hello".endsWith?("llo") # => true "hello".endsWith?("hel") # => false ``` _Selecting files by extension_ ```kex paths.filter { |p| p.endsWith?(".kex") } ``` ### From [`Monoid`](algebra.md#trait-monoid) - [`repeat`](algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Comparable`](comparable.md#trait-comparable) - [`max`](comparable.md#comparable-max) — What every ordered type gets from `compare` alone. - [`min`](comparable.md#comparable-min) — Returns the smaller of this value and `other`; this value when they are equal. - [`clamp`](comparable.md#comparable-clamp) — Returns this value limited to the range `low` to `high`: `low` when it is below, `high` when it is above, and itself otherwise. - [`<`](comparable.md#comparable-op-lt) — The ordering operators, from `compare` alone: whatever is `Comparable` can be written with `<` and `>`, a record as much as a number. - [`>`](comparable.md#comparable-op-gt) — - [`<=`](comparable.md#comparable-op-lt-eq) — - [`>=`](comparable.md#comparable-op-gt-eq) — ### From [`Enumerable`](enumerable.md#trait-enumerable) - [`map`](enumerable.md#enumerable-map) — Applies `f` to each item and collects the results into a list. - [`mapIndexed`](enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`flatMap`](enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](enumerable.md#trait-foldable) - [`each`](enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. ### Defined in other modules - [Algebra](algebra.md#make-string): [`identity`](algebra.md#string-identity), [`combine`](algebra.md#string-combine) - [Blankable](blankable.md#make-string): [`blank?`](blankable.md#string-blank?) - [Comparable](comparable.md#make-string): [`compare`](comparable.md#string-compare) - [Truthyable](truthyable.md#make-string): [`truthy?`](truthyable.md#string-truthy?) ## type `Char` A single Unicode character. Character literals are written with single quotes (`'a'`) and are a different type from the one-character string `"a"`. The classification functions (`digit?`, `alpha?`, `space?` and the rest) are what most character code needs; `codepoint` and `String.fromCodepoint` are the escape hatch to raw Unicode values. ```kex "hello world".chars.count(~alpha?) # => 10 'a'.upperCase # => 'A' 'a'.string # => "a" ``` ### `string` ```kex string : String ``` Returns this character as a one-character `String`. A `Char` and a `String` are distinct types, so converting between them is explicit. `to(String)` is the fallible conversion protocol and answers `String?` like every other conversion; `string` is the total one, because a character is always one character of text and has no failure case to report. It is the single-character counterpart of `String.chars` and `[Char].join("")`. **Returns**: the character as text **Examples** ```kex 'a'.string # => "a" 'a'.to(String) # => Just("a") "hi".chars.map(&.string) # => ["h", "i"] ``` ### `upperCase` ```kex upperCase : Char ``` Returns the upper-case form of this character, still as a `Char`. Characters with no upper-case form (digits, punctuation) are returned unchanged. **Returns**: the upper-case character **Examples** ```kex 'a'.upperCase # => 'A' '1'.upperCase # => '1' ``` ### `lowerCase` ```kex lowerCase : Char ``` Returns the lower-case form of this character, still as a `Char`. Characters with no lower-case form are returned unchanged. **Returns**: the lower-case character **Examples** ```kex 'A'.lowerCase # => 'a' '!'.lowerCase # => '!' ``` ### `digit?` ```kex digit? : Bool ``` Returns `true` when the character is a decimal digit, `'0'` through `'9'`. **Returns**: `true` for a decimal digit **Examples** ```kex '3'.digit? # => true 'a'.digit? # => false ``` _Validating that a field is all digits_ ```kex "12345".chars.all?(~digit?) # => true ``` ### `alpha?` ```kex alpha? : Bool ``` Returns `true` when the character is alphabetic. **Returns**: `true` for a letter **Examples** ```kex 'a'.alpha? # => true '1'.alpha? # => false ' '.alpha? # => false ``` _Keeping only letters_ ```kex "Hello, World!".filter(~alpha?) # => "HelloWorld" ``` ### `letter?` ```kex letter? : Bool ``` Returns `true` when the character is a letter. The Unicode-letter spelling of `alpha?`. **Returns**: `true` for a letter **Examples** ```kex 'z'.letter? # => true '-'.letter? # => false ``` ### `upper?` ```kex upper? : Bool ``` Returns `true` when the character is upper case. Characters without case (digits, punctuation) answer `false`. **Returns**: `true` for an upper-case letter **Examples** ```kex 'A'.upper? # => true 'a'.upper? # => false '1'.upper? # => false ``` _Splitting a camelCase name_ ```kex "camelCaseName".findIndex(~upper?) # => Just(5) ``` ### `lower?` ```kex lower? : Bool ``` Returns `true` when the character is lower case. **Returns**: `true` for a lower-case letter **Examples** ```kex 'a'.lower? # => true 'A'.lower? # => false ``` ### `space?` ```kex space? : Bool ``` Returns `true` when the character is whitespace: a space, tab, newline or carriage return. **Returns**: `true` for whitespace **Examples** ```kex ' '.space? # => true '\t'.space? # => true 'a'.space? # => false ``` _Counting the words on a line_ ```kex "one two three".split(" ").reject(~empty?).count # => 3 ``` ### `codepoint` ```kex codepoint : Integer ``` Returns the character's Unicode codepoint. `String.fromCodepoint` is the inverse. **Returns**: the codepoint **Examples** ```kex 'A'.codepoint # => 65 'é'.codepoint # => 233 ``` _Converting a digit character to its value_ ```kex '7'.codepoint - '0'.codepoint # => 7 ``` ### `in?` ```kex in?(range: Range) -> Bool ``` Returns `true` when the character falls inside `range`, endpoints included. **Parameters** - `range` — the inclusive character range **Returns**: `true` when the character is in range **Examples** ```kex 'b'.in?('a'..'z') # => true 'B'.in?('a'..'z') # => false ``` _A hexadecimal-digit test_ ```kex let hex?(c: Char) -> Bool = c.digit? || c.lowerCase.in?('a'..'f') ``` ## module `String` Constructors for `String` values that are built from something other than text: a Unicode codepoint, or raw UTF-8 bytes. ### `fromCodepoint` ```kex fromCodepoint(value: Integer) -> String? ``` Builds a one-character string from a Unicode codepoint. Answers `None` for a surrogate or a value outside the Unicode scalar range, so the result is always valid text. `Char.codepoint` is the inverse. **Parameters** - `value` — the Unicode codepoint **Returns**: the one-character string, or `None` **Examples** ```kex String.fromCodepoint(65) # => Just("A") String.fromCodepoint(233) # => Just("é") String.fromCodepoint(-1) # => None ``` _Building the alphabet_ ```kex (0..25).items.map { |i| String.fromCodepoint(97 + i).or("") }.join("") # => "abcdefghijklmnopqrstuvwxyz" ``` ### `fromBytes` ```kex fromBytes(values: [Byte]) -> String? ``` Rebuilds a string from its UTF-8 bytes: the inverse of `bytes`. A `String` is TEXT, so bytes outside 0..255 or a malformed encoding answer `None` rather than a string that would decode to replacement characters. That makes it a decoding step you can check, not a lossy cast. **Parameters** - `values` — the UTF-8 bytes **Returns**: the decoded string, or `None` **Examples** ```kex String.fromBytes([104, 105]) # => Just("hi") String.fromBytes([195, 169]) # => Just("é") String.fromBytes([255]) # => None ``` _A round trip_ ```kex String.fromBytes("héllo".bytes) # => Just("héllo") ``` ## type `Tuple` A fixed-size group of values, written `(a, b)`. Unlike a list, a tuple's size and the type of each position are part of its type, so the `List` functions do not apply to it: destructure it, match on it, or convert it with `items`. ```kex let (name, age) = ("Ada", 36) [1, 2, 3].partition { |n| n.even? } # => ([2], [1, 3]) ``` ### `items` ```kex items : [Any] ``` Returns the tuple's elements as a list. A tuple is not a list (its arity is part of its type) so the `List` functions do not apply to it. This is the explicit conversion, and it loses the per-position typing in exchange. Destructuring is usually clearer when you know the shape: `let (a, b) = pair`. **Returns**: the elements, in order **Examples** ```kex (1, "a").items # => [1, "a"] ``` _Iterating over a pair_ ```kex ("host", "port").items.each { |s| IO.printLine(s) } ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: system.kex title: System entities: - { kind: module, name: "System" } - { kind: function, name: "die" } --- # System ## module `System` The running process and the machine under it: exiting, and asking what platform this is. ```kex System.OS # => :macos System.posix? # => true System.exit(1) # ends the program with status 1 ``` ### `exit` ```kex exit(code: Integer) -> Void ``` Ends the process immediately with exit status `code`. The invoking shell receives the code, so this is how a command-line tool reports success or failure to whatever ran it: 0 means success, anything else means failure. Nothing after the call runs, and no cleanup happens: close what needs closing first. **Parameters** - `code` — the exit status **Examples** _Failing with a message_ ```kex IO.printError("config not found") System.exit(1) ``` _Reporting a test run's result_ ```kex System.exit(failures.empty? then 0 else 1) ``` ### `OS` (constant) ```kex OS : OperatingSystem ``` The operating system family this program is running on. Both backends answer with the same atom for the same machine. Prefer the `macOS?` / `linux?` / `windows?` / `posix?` predicates below when you are asking one yes-or-no question; use `OS` when you need to branch several ways. **Examples** ```kex System.OS # => :macos ``` _Choosing a config location per platform_ ```kex match System.OS do :macos => "~/Library/Application Support/app" :linux => "~/.config/app" _ => "." end ``` ### `BITWIDTH` (constant) ```kex BITWIDTH : Integer ``` The machine's pointer width in bits: 64 on anything current, 32 on a small target. Reported by the emulator on BEAM and by the pointer size in the tree walker, so both agree for one machine. **Examples** ```kex System.BITWIDTH # => 64 ``` ### `macOS?` (constant) ```kex macOS? : Bool ``` Returns `true` when running on macOS. **Examples** ```kex let opener = System.macOS? then "open" else "xdg-open" ``` ### `linux?` (constant) ```kex linux? : Bool ``` Returns `true` when running on Linux. **Examples** ```kex if System.linux? IO.printLine("using /proc") end ``` ### `windows?` (constant) ```kex windows? : Bool ``` Returns `true` when running on Windows. **Examples** ```kex let separator = System.windows? then "\\" else "/" ``` ### `posix?` (constant) ```kex posix? : Bool ``` Returns `true` when the platform follows POSIX conventions for paths, separators and shell behaviour. Everything the toolchain runs on except Windows behaves POSIX-ly enough for paths, separators and shell conventions. An unknown system is NOT assumed to be POSIX. **Examples** ```kex System.posix? # => true on macOS and Linux ``` _Refusing to guess on an unrecognised platform_ ```kex if !System.posix? IO.printError("this tool assumes POSIX paths") System.exit(1) end ``` ## type `OperatingSystem` The operating system families a program may be running on. A union of atoms rather than an ADT: these are plain tags, and a `match` over them is still exhaustive. Anything unmodelled is `:unknown`: the union is closed, so callers can cover it. **Examples** _Branching on the platform_ ```kex match System.OS do :windows => "\\" :unknown => die("unsupported platform") _ => "/" end ``` ## function `die` ```kex die : Never die(message: String) -> Never ``` Ends the program with a fatal error message. `message` goes to stderr behind a `"fatal: "` prefix and the exit status is 1. This is an abort, not an exception, so `trying` / `rescue` cannot catch it. Use it only where there is no recoverable answer. The prelude uses it for a negative `repeat` count, for instance. Its result type is `Never`, the bottom type: `die` does not return, so a branch that dies takes the other branch's type, and `if b == 0 then die("divide by zero") else a end` is an `Integer`. `die` is declared bare, like `assert`, so it is always in scope. **Parameters** - `message` — the fatal message; omitted for a generic one **Returns**: never returns **Examples** ```kex die("unreachable state") # stderr: fatal: unreachable state ``` _Using it in a branch that must produce a value_ ```kex let denominator = if divisor == 0 then die("divide by zero") else divisor end ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: taggedvalidation.kex title: TaggedValidation entities: - { kind: module, name: "TaggedValidation" } --- # TaggedValidation ## module `TaggedValidation` Diagnostics returned by compile-time validators for tagged literals. A tagged literal (`` re`\d+` ``, `` sql`SELECT ...` ``) can be checked while your program is compiled rather than when it runs. The compiler finds the checker by name: a tag `foo` is validated by a function named `validateFoo` taking the literal's text and returning a list of `Issue` values. An empty list means the literal is fine. ```kex let validateHex(source: String) -> [TaggedValidation.Issue] do match source.chars.findIndex { |c| !c.digit? && !c.in?('a'..'f') } do Just(offset) => [TaggedValidation.fatalAt(offset, "not a hex digit")] None => [] end end ``` A `Fatal` issue stops the build with the message, pointing the caret at the offset; a `Warn` reports without stopping. Byte offsets are zero-based and relative to the cooked literal body. ### `fatal` ```kex fatal(message: String) -> Issue ``` A fatal issue about the literal as a whole, with no particular position. **Parameters** - `message` — what is wrong **Returns**: the diagnostic **Examples** ```kex TaggedValidation.fatal("an empty pattern matches nothing") ``` ### `fatalAt` ```kex fatalAt(offset: Integer, message: String) -> Issue ``` A fatal issue at one offset in the literal. The offset is where the caret points in the compiler's error, so use the position the underlying checker reported. **Parameters** - `offset` — the zero-based byte offset in the literal body - `message` — what is wrong **Returns**: the diagnostic **Examples** ```kex TaggedValidation.fatalAt(3, "missing closing parenthesis") ``` _Reporting what a checker found_ ```kex match Kex.Intrinsic.Regex.validate(source) do Just((offset, message)) => [TaggedValidation.fatalAt(offset, message)] None => [] end ``` ### `fatalBetween` ```kex fatalBetween : Integer -> Integer -> String -> Issue ``` A fatal issue spanning a range of the literal. **Parameters** - `start` — the first byte offset of the span - `finish` — the offset just past its end - `message` — what is wrong **Returns**: the diagnostic **Examples** ```kex TaggedValidation.fatalBetween(0, 4, "this group can never match") ``` ### `warn` ```kex warn(message: String) -> Issue ``` A warning about the literal as a whole. Reported, but the build continues. **Parameters** - `message` — what is questionable **Returns**: the diagnostic **Examples** ```kex TaggedValidation.warn("this pattern is very slow on long inputs") ``` ### `warnAt` ```kex warnAt(offset: Integer, message: String) -> Issue ``` A warning at one offset in the literal. **Parameters** - `offset` — the zero-based byte offset in the literal body - `message` — what is questionable **Returns**: the diagnostic **Examples** ```kex TaggedValidation.warnAt(7, "redundant escape") ``` ### `warnBetween` ```kex warnBetween : Integer -> Integer -> String -> Issue ``` A warning spanning a range of the literal. **Parameters** - `start` — the first byte offset of the span - `finish` — the offset just past its end - `message` — what is questionable **Returns**: the diagnostic **Examples** ```kex TaggedValidation.warnBetween(2, 6, "this alternation is always taken") ``` ## type `ByteSpan` Where in the literal an issue applies: a single offset, or a range. Both are byte offsets into the literal body, counted from zero. **Variants** - `At(Integer)` - `Between(Integer, Integer)` ## type `Issue` One diagnostic about a tagged literal. `Fatal` stops the build; `Warn` reports and lets it continue. The span is optional: an issue about the literal as a whole carries `None`. **Variants** - `Fatal(ByteSpan?, String)` - `Warn(ByteSpan?, String)` --- --- package: prelude version: "0.4.0-rc.3-dev" source: template.kex title: Template entities: - { kind: module, name: "Template" } --- # Template ## module `Template` An ERB-shaped template scanner: template text in, a template AST out. Opt-in: nothing here is in scope until `using Template`. This is the scanning stage only (see the Template proposal for the fuller design): it turns template source into a flat list of +Node+s: plain text, and the four kinds of `<% %>` region, plus whatever frontmatter tags sit ahead of the body. It does not evaluate anything and does not know Kex syntax; the text inside a hole is kept as-is, for a later stage to parse and lower into real Kex. ```kex using Template let source = "---\ntitle: Welcome\nparams: [name, library: Bool]\n---\nHi <%= name %>!" let parsed = Template.scan(source).try parsed.frontmatter.get("title") # => Just(Scalar("Welcome")) parsed.parameters # => [TemplateParam { name: "name", type: "" }, # TemplateParam { name: "library", type: "Bool" }] parsed.nodes # => [Text("Hi "), Interpolate("name"), Text("!")] ``` A template file carries its host's extension ahead of `.ket`, so an editor can highlight it as what it is — `README.md.ket`, `profile.html.ket`: ```kex --- params: [name, library: Bool, dependencies: [Dependency]] --- # <%= name %> <% if library %> A Kex library. <% else %> A Kex application. <% end %> <% dependencies.map do |dep| %> - `<%= dep.name %>` ~> <%= dep.version %> <% end %> ``` ## Syntax ```kex <%= expr %> interpolate (escaping is a later stage's job) <%== expr %> interpolate raw, no escaping <% ... %> a Kex control region: `if`/`match` arms, block bodies, `let` <%# ... %> comment, emits nothing <%- ... -%> whitespace control: trims the line's leading indent before the tag, and the newline right after it. A `<% %>` or `<%# %>` tag standing alone on its line does this on its own, so the markers are for a tag sharing its line with real content <%% a literal `<%`, for a template that generates ERB-shaped output itself ``` ## Layouts A layout is an ordinary template that takes the page as a block. Neither template reads the other: the Kex code that calls them composes them, and the block's result is inserted raw, since it is already rendered (and, from `Template.html`, already escaped). ```kex --- params: [title: String, content: Block] --- <%= title %>
<%== content() %>
let frame = Template.html(Kex.embed("views/layouts/auth.html.ket")) let form = Template.html(Kex.embed("views/signin.html.ket")) let signin(email: String) -> String = frame("Sign in") { form(email) } ``` ## Whitespace A tag that emits nothing — `<% %>` and `<%# %>` — and stands alone on its line takes that line with it. Only whitespace may share the line with it: the indent before it and the newline after it are scaffolding, never content, so a template reads the way its output does: ```kex <% if dependencies.count > 0 %> ## Dependencies <% end %> # => "## Dependencies\n" — no blank line where the tags were ``` The `<%- -%>` markers are for the case this does not cover: a tag sharing its line with real content, where what to trim is a judgement call rather than obvious. ```kex Total: <%= total %> <%- if pending > 0 %>(<%= pending %> pending)<% end %> ``` An interpolation is never trimmed, on its own line or not. It stands in for content, so the whitespace around it is content too: ```kex <%= greeting %> <%= name %> # => "Hi\nAda\n" — both newlines survive ``` ## Frontmatter A template file may open with a `---` line, generic `key: value` tags up to a closing `---` line, and then the body. This is metadata for whatever reads the template (a title, a list of tags) and there is nothing template-specific about it: it is scanned the same generic way whatever key is used. ```kex --- title: Release notes tags: [changelog, public] --- # <%= title %> ``` A template's parameters, when a later stage needs them declared rather than inferred, are just another frontmatter key: conventionally `params`, a list of names each with an optional `: Type`: ```kex --- params: [name, library: Bool, dependencies: [Dependency]] --- # <%= name %> ``` Nothing in the scanner treats `params` specially while scanning: it is metadata like any other key, a `[a, b, c]` list same as `tags` above. `Parsed#parameters` is a convenience reader for it: it splits each entry on its first colon into a `TemplateParam { name, type }` (`type` is `""` for a bare name), so a caller does not need to know the key name, match on `Tag` itself, or split each entry by hand. `type` is raw text, same as everywhere else in this module: turning `[Dependency]` into a real, resolved Kex type is later work, not this module's. ### `scan` ```kex scan(source: String) -> Result ``` Scans template source into a `Parsed` template, or says where it broke. **Parameters** - `source` — the template file's full text **Returns**: the scanned template, or why not **Examples** ```kex Template.scan("Hi <%= name %>!").map(~nodes) # => Ok([Text("Hi "), Interpolate("name"), Text("!")]) ``` _A control tag on a line of its own leaves no blank line behind_ ```kex Template.scan("<% if admin %>\nWelcome back.\n<% end %>\n").try.nodes # => [Control("if admin"), Text("Welcome back.\n"), Control("end")] ``` _Where the template broke, as a position in the source_ ```kex Template.scan("Hi <%= name") # => Error(UnterminatedTag(6)) ``` ### `escapeHtml` ```kex escapeHtml(text: String) -> String ``` Escapes `&`, `<`, `>`, double quotes and single quotes for HTML text and quoted attribute values. `Template.html` escapes `<%= %>` holes this way; `<%== %>` inserts text without escaping. This does not validate URLs or escape JavaScript or CSS. **Parameters** - `text` — the text to escape **Returns**: the same text, HTML-safe **Examples** ```kex Template.escapeHtml("Tom & Jerry") # => "<b>Tom & Jerry</b>" ``` ### `renderParsed` ```kex renderParsed(parsed: Parsed, context: {String: String}) -> Result ``` Fills an already-scanned template from a runtime `context` map. `<%= %>` inserts HTML-escaped text and `<%== %>` inserts raw text. Runtime rendering supports substitutions only: a `<% ... %>` control region returns `UnsupportedControl`. Use `Template.html` or `Template.text` for templates with Kex control flow that are known at compile time. Hole names are trimmed and looked up as complete keys. A hole written `<%= dep.name %>` reads the `"dep.name"` key, rather than a field of a value stored under `"dep"`. **Parameters** - `parsed` — a template already scanned by `Template.scan` - `context` — a value for every `<%= %>`/`<%== %>` hole the template uses, keyed by the hole's exact (trimmed) text **Returns**: the rendered text, or why not **Examples** ```kex let parsed = Template.scan("Hi <%= name %>!").try Template.renderParsed(parsed, { "name": "" }) # => Ok("Hi <Ada>!") ``` _A hole `context` does not cover_ ```kex Template.renderParsed(Template.scan("<%= missing %>").try, {}) # => Error(UndefinedVariable("missing")) ``` _Control flow is refused, not silently skipped_ ```kex Template.renderParsed(Template.scan("<% if x %>y<% end %>").try, {}) # => Error(UnsupportedControl("if x")) ``` ### `render` ```kex render(source: String, context: {String: String}) -> Result ``` `Template.scan(source).try` then `renderParsed` — see its doc comment for what this does and, as importantly, what it refuses to do. **Parameters** - `source` — the template's full text, scanned fresh - `context` — see `renderParsed` **Returns**: the rendered text, or why not **Examples** ```kex Template.render("Hi <%= name %>!", { "name": "Ada" }) # => Ok("Hi Ada!") ``` ## type `Node` One piece of a scanned template. Hole and control content is carried as raw text: the Kex source that was between the delimiters, trimmed of surrounding whitespace. Turning that text into real, type-checked Kex expressions is later work; a `Node` only records what KIND of region it is and what text it held. **Variants** - `Text(String)` - `Interpolate(String)` - `InterpolateRaw(String)` - `Control(String)` - `Comment(String)` ## type `Tag` A frontmatter value: a plain scalar, or a `[a, b, c]` list. **Variants** - `Scalar(String)` - `Tags([String])` ## type `TemplateError` Why a template's text could not be scanned or rendered, and where (or, for `render`/`renderParsed`, what stopped it). **Variants** - `UnterminatedTag(Integer)` - `UnterminatedFrontmatter` - `MalformedFrontmatterLine(String)` - `UndefinedVariable(String)` - `UnsupportedControl(String)` ## record `TemplateParam` One entry from a `params: [...]` frontmatter list: a name, and its optional `: Type` annotation. `type` is raw text: `""` for a bare name, never a resolved Kex type: the same way a frontmatter `Tag` is text and not a parsed value. **Fields** - `name` : [String](string.md#make-string) - `type` : [String](string.md#make-string) ## record `TagScan` One scanned `<% ... %>` region: its node, and the whitespace trims its delimiters asked for. It is internal to scanning, not part of the public API, but declared at module scope rather than inside `private do`. Nested there, this record's fields lose their names under BEAM codegen and the value round-trips as an untagged tuple (`Undefined method: leftTrim for Tuple`), even though the same code is fine on the tree-walking interpreter. **Fields** - `node` : Node - `leftTrim` : [Bool](truthyable.md#make-bool) - `rightTrim` : [Bool](truthyable.md#make-bool) ## record `Parsed` A scanned template: its frontmatter tags, and its body as a node list. **Fields** - `frontmatter` : {[String](string.md#make-string): [Tag](#type-template-tag)} - `nodes` : [Node] ### Functions #### `parameters` ```kex parameters : [TemplateParam] ``` The template's declared parameters, out of a `params: [...]` frontmatter key: `[]` when the template declares none, or when `params` holds a bare scalar rather than a list. **Returns**: the declared parameters, in the order written **Examples** ```kex parsed.parameters # => [TemplateParam { name: "name", type: "" }, TemplateParam { name: "library", type: "Bool" }] ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: test.kex title: Assert entities: - { kind: function, name: "describe" } - { kind: function, name: "it" } - { kind: function, name: "before" } - { kind: function, name: "after" } - { kind: function, name: "assert" } - { kind: module, name: "Assert" } --- # Assert The built-in testing DSL: `describe`, `it`, `before`, `after`, and assertion helpers. Everything here is always in scope: no import, and no separate test runner. Write a test file and run it with `kex`: ```kex describe "arithmetic" do it "adds numbers" do assert(1 + 1 == 2) end it "multiplies numbers" do Assert.equal(3 * 4, 12) end end $ kex my_test.kex arithmetic ✓ adds numbers ✓ multiplies numbers ``` Output uses ✓ / ✗ markers and nests by `describe` depth. A failing `assert` marks its test failed and moves on; the rest of the suite still runs. A file named `.spec.kex` automatically loads the declarations of `.kex` beside it, so a spec needs no import and no `main` wrapper. ## function `describe` ```kex describe(label: String, block: Block) -> Void ``` Groups related test cases under a label, and runs them. The block is called immediately. `describe` blocks nest, and the output is indented to match. This is a foul function: it prints. **Parameters** - `label` — what this group is about - `block` — the tests, and any nested groups **Examples** ```kex describe "String" do describe "count" do it "returns the character count" do assert("hello".count == 5) end end end ``` ## function `it` ```kex it(label: String, block: Block) -> Void ``` Defines a single test case, and runs it. The block runs, and anything thrown inside it: typically a failed `assert`: marks the test failed without aborting the rest of the suite. This is a foul function: it prints. **Parameters** - `label` — what this case establishes, phrased as a claim - `block` — the case body **Examples** ```kex it "returns true for even numbers" do assert(2.even?) assert(!3.even?) end ``` _One behaviour per case reads better than one big case_ ```kex it "trims leading whitespace" do Assert.equal(" hi".trim, "hi") end it "trims trailing whitespace" do Assert.equal("hi ".trim, "hi") end ``` ## function `before` ```kex before(block: Block) -> Void before(block: Atom) -> Block -> Void ``` Registers setup to run before the current group's tests. `before { ... }` and `before(:each) { ... }` run before every test in the group; `before(:all) { ... }` runs once before the group's first test, following RSpec's scope convention. **Parameters** - `block` — the setup to run **Examples** ```kex describe "the parser" do before do Mock.FS.file("input.txt", "one\ntwo\n") end it "reads two lines" do Assert.equal(FS.File.readLines("input.txt").or([]).count, 2) end end ``` ## function `after` ```kex after(block: Block) -> Void after(block: Atom) -> Block -> Void ``` Registers cleanup to run after the current group's tests. Defaults to `:each`. Cleanup is unconditional: it runs whether the test passed or failed, and inner per-test hooks run before outer hooks. **Parameters** - `block` — the cleanup to run **Examples** ```kex after do FS.File.delete("tmp/out.txt") end ``` ## function `assert` ```kex assert(value: Bool) -> Bool assert(value: Bool) -> String -> Bool ``` Fails the enclosing `it` when `value` is falsy. The primitive every other assertion is built on. Prefer the `Assert` helpers where one fits: they report what was expected and what arrived, which a bare `assert` cannot. **Parameters** - `value` — the condition that must hold **Returns**: the value, when it held **Examples** ```kex assert(42 == 42) assert("hello".count == 5) ``` ## module `Assert` Focused assertions, each reporting what was expected and what arrived. These are ordinary Kex stdlib functions layered on the primitive `assert`, so adding another helper does not require compiler or runtime work. ```kex Assert.equal("hi".upperCase, "HI") Assert.some(users.first) Assert.ok(Integer.parse("42")) ``` ### `equal` ```kex equal(actual: A, expected: A) -> Bool ``` Fails unless `actual` equals `expected`, reporting both. The assertion to reach for by default: a failure tells you what arrived, which `assert(a == b)` does not. **Parameters** - `actual` — the value produced - `expected` — the value it should equal **Returns**: `true` when they are equal **Examples** ```kex Assert.equal("hi".upperCase, "HI") Assert.equal([1, 2, 3].sum, 6) Assert.equal(config.get(:port), Just(8080)) ``` ### `notEqual` ```kex notEqual(actual: A, expected: A) -> Bool ``` Fails when `actual` equals `expected`. **Parameters** - `actual` — the value produced - `expected` — the value it should differ from **Returns**: `true` when they differ **Examples** ```kex Assert.notEqual(newId, oldId) ``` ### `truthy` ```kex truthy(value: A) -> Bool ``` Fails unless `value` is truthy: anything except `false`, `None` and `()`. **Parameters** - `value` — the value to test **Returns**: `true` when the value is truthy **Examples** ```kex Assert.truthy("hello".contains?("ell")) ``` ### `falsy` ```kex falsy(value: A) -> Bool ``` Fails unless `value` is falsy: `false`, `None` or `()`. **Parameters** - `value` — the value to test **Returns**: `true` when the value is falsy **Examples** ```kex Assert.falsy([].any? { |x| x > 0 }) ``` ### `some` ```kex some(value: A?) -> Bool ``` Fails unless `value` is a `Just`. **Parameters** - `value` — the optional to test **Returns**: `true` when a value is present **Examples** ```kex Assert.some([1, 2].first) Assert.some(config.get(:host)) ``` ### `none` ```kex none(value: A?) -> Bool ``` Fails unless `value` is `None`. **Parameters** - `value` — the optional to test **Returns**: `true` when there is no value **Examples** ```kex Assert.none([].first) Assert.none("abc".to(Integer)) ``` ### `ok` ```kex ok(value: Result) -> Bool ``` Fails unless `value` is an `Ok`. **Parameters** - `value` — the result to test **Returns**: `true` when the result succeeded **Examples** ```kex Assert.ok(Integer.parse("42")) ``` ### `error` ```kex error(value: Result) -> Bool ``` Fails unless `value` is an `Error`. **Parameters** - `value` — the result to test **Returns**: `true` when the result failed **Examples** ```kex Assert.error(Integer.parse("4x")) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: time.kex title: Time entities: - { kind: type, name: "Weekday" } - { kind: type, name: "TimeError" } - { kind: record, name: "Date" } - { kind: record, name: "Time" } - { kind: record, name: "DateTime" } - { kind: record, name: "Period" } - { kind: module, name: "Time" } - { kind: module, name: "Date" } - { kind: module, name: "DateTime" } - { kind: make, name: "Integer" } - { kind: make, name: "Float" } - { kind: make, name: "Duration" } - { kind: module, name: "Duration" } - { kind: module, name: "Period" } - { kind: make, name: "Period" } - { kind: make, name: "Date" } - { kind: make, name: "Time" } - { kind: make, name: "DateTime" } --- # Time Calendar dates, wall-clock times, and instants. Three civil types, each a plain record: ```kex Date a calendar day, no time and no zone (2026-07-30) Time a time of day, no date and no zone (14:03:00) DateTime both, plus a fixed offset from UTC (2026-07-30T14:03:00+02:00) ``` Two span types connect them, and which one you want depends on whether the calendar gets a say: ```kex Duration fixed elapsed time, a count of seconds (36.hours, 10.days) Period a calendar step, resolved by the calendar (1.months, 2.years) ``` `36.hours` is always 129600 seconds; `1.months` is however long that particular month turns out to be. So `date + 1.months` clamps January 31st to the last day of February, while `date + 30.days` counts thirty days. A time `Measure` such as `5.second` is a third thing and deliberately NOT a Duration: a Measure describes a measurement, a Duration describes elapsed time. The plural `5.seconds` builds the Duration. Values are built through their own module and used through functions: ```kex let due = Date.of(2026, 7, 30).try # Date due.weekday.name # "Thursday" (due + 10.days).iso # "2026-08-09" (due + 1.months).iso # "2026-08-30" Time.now().iso # e.g. "14:03:00" ``` Anything that reads the clock is replaceable: `Time.Clock` is a capability, and `with Time.Clock = Mock.Clock { at: ... }` pins it for a block. Zones are fixed offsets: UTC, an explicit `+02:00`, or whatever this machine's zone resolves to at a given instant. Named IANA zones and their DST rules are not modeled: `Time.now()` asks the host for the offset in effect at that moment, so it is right now, but it cannot say what the offset WILL be for some future local time. The records and the two ADTs stay at file level so `make` blocks, callers, and every module here can see them. ## type `Weekday` Weekday names in ISO order (Monday is day 1). **Variants** - `Monday` - `Tuesday` - `Wednesday` - `Thursday` - `Friday` - `Saturday` - `Sunday` ## type `TimeError` A field out of range, or text that is not a date/time. **Variants** - `InvalidDate(Integer, Integer, Integer)` - `InvalidTime(Integer, Integer, Integer)` - `InvalidFormat(String)` ## record `Date` A calendar day: a year, a month and a day, with no time and no zone. ```kex let due = Date.of(2026, 7, 30).try due.iso # => "2026-07-30" due.weekday.name # => "Thursday" (due + 10.days).iso # => "2026-08-09" ``` Build one with `Date.of`, which validates, rather than with the record literal, which does not. **Fields** - `year` : [Integer](number.md#make-integer) - `month` : [Integer](number.md#make-integer) - `day` : [Integer](number.md#make-integer) Implements [`Inspectable`](kex.md#trait-inspectable), [`Showable`](kex.md#trait-showable). ### Functions #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Renders the date structurally, for debugging output. **Parameters** - `colors` — whether to include ANSI color escapes **Returns**: the rendered date #### `showValue` (from Showable) ```kex showValue : String ``` Renders the date in ISO form, so interpolation and `IO.printLine` show `2026-07-30`. **Returns**: the ISO 8601 date **Examples** ```kex "due ${Date.of(2026, 7, 30).try}" # => "due 2026-07-30" ``` #### `epochDay` ```kex epochDay : Integer ``` Days since 1970-01-01, negative before it. The date's identity as a number: comparison and day arithmetic go through it. **Returns**: days since the Unix epoch **Examples** ```kex Date.of(1970, 1, 1).try.epochDay # => 0 Date.of(2026, 7, 30).try.epochDay # => 20664 ``` #### `weekday` ```kex weekday : Weekday ``` The day of the week this date falls on. **Returns**: the day of the week **Examples** ```kex Date.of(2026, 7, 30).try.weekday # => Thursday Date.of(2026, 7, 30).try.weekday.name # => "Thursday" ``` #### `dayOfYear` ```kex dayOfYear : Integer ``` The day's position in its year, counting from 1 on January 1st. **Returns**: the day of the year **Examples** ```kex Date.of(2026, 1, 1).try.dayOfYear # => 1 Date.of(2026, 7, 30).try.dayOfYear # => 211 ``` #### `leapYear?` ```kex leapYear? : Bool ``` Returns `true` when this date falls in a leap year. **Returns**: `true` in a leap year **Examples** ```kex Date.of(2024, 1, 1).try.leapYear? # => true Date.of(2026, 1, 1).try.leapYear? # => false ``` #### `daysInMonth` ```kex daysInMonth : Integer ``` The number of days in THIS date's month. No Result: a Date's month is in range by construction, unlike `Time.daysInMonth`'s two loose integers. **Returns**: the length of this month **Examples** ```kex Date.of(2024, 2, 1).try.daysInMonth # => 29 Date.of(2026, 7, 1).try.daysInMonth # => 31 ``` #### `+` ```kex +(span: Duration) -> Date ``` Advances the date by a fixed span, whole days only. A Duration with a sub-day remainder truncates toward zero, so +date + 36.hours+ advances exactly one day. Use a `Period` when the calendar should get a say. **Parameters** - `span` — the elapsed span to add **Returns**: the later date **Examples** ```kex (Date.of(2026, 7, 30).try + 10.days).iso # => "2026-08-09" (Date.of(2026, 7, 30).try + 36.hours).iso # => "2026-07-31" ``` #### `-` ```kex -(span: Duration) -> Date ``` Moves the date back by a fixed span, whole days only. **Parameters** - `span` — the elapsed span to subtract **Returns**: the earlier date **Examples** ```kex (Date.of(2026, 7, 30).try - 10.days).iso # => "2026-07-20" ``` #### `addDays` ```kex addDays(count: Integer) -> Date ``` The date `count` days later. A negative count moves backwards. **Parameters** - `count` — how many days to add **Returns**: the shifted date **Examples** ```kex Date.of(2026, 7, 30).try.addDays(1).iso # => "2026-07-31" Date.of(2026, 7, 30).try.addDays(-1).iso # => "2026-07-29" ``` #### `addWeeks` ```kex addWeeks(count: Integer) -> Date ``` The date `count` weeks later. A negative count moves backwards. **Parameters** - `count` — how many weeks to add **Returns**: the shifted date **Examples** ```kex Date.of(2026, 7, 30).try.addWeeks(2).iso # => "2026-08-13" ``` #### `addMonths` ```kex addMonths(count: Integer) -> Date ``` No `date.tomorrow`/`date.yesterday` functions: on BEAM a make-block function flattens onto the same name as the `Date.tomorrow()`/`Date.yesterday()` module functions above and one of the two has to win. The module functions win: `Date.tomorrow()` is the spelling people reach for, and `date.addDays(1)` already says the rest. The date `count` calendar months later, with the day clamped into the target month. One month after January 31st is the last day of February, not March 3rd. A negative count moves backwards. **Parameters** - `count` — how many months to add **Returns**: the shifted date **Examples** ```kex Date.of(2026, 1, 31).try.addMonths(1).iso # => "2026-02-28" Date.of(2026, 7, 30).try.addMonths(-1).iso # => "2026-06-30" ``` #### `addYears` ```kex addYears(count: Integer) -> Date ``` The date `count` calendar years later, with the day clamped: February 29th plus one year is February 28th. **Parameters** - `count` — how many years to add **Returns**: the shifted date **Examples** ```kex Date.of(2024, 2, 29).try.addYears(1).iso # => "2025-02-28" ``` #### `startOfMonth` ```kex startOfMonth : Date ``` The first day of this date's month. **Returns**: the first of the month **Examples** ```kex Date.of(2026, 7, 30).try.startOfMonth.iso # => "2026-07-01" ``` #### `endOfMonth` ```kex endOfMonth : Date ``` The last day of this date's month, whatever its length. **Returns**: the last of the month **Examples** ```kex Date.of(2026, 7, 30).try.endOfMonth.iso # => "2026-07-31" Date.of(2024, 2, 1).try.endOfMonth.iso # => "2024-02-29" ``` #### `startOfYear` ```kex startOfYear : Date ``` January 1st of this date's year. **Returns**: the first of the year **Examples** ```kex Date.of(2026, 7, 30).try.startOfYear.iso # => "2026-01-01" ``` #### `endOfYear` ```kex endOfYear : Date ``` December 31st of this date's year. **Returns**: the last of the year **Examples** ```kex Date.of(2026, 7, 30).try.endOfYear.iso # => "2026-12-31" ``` #### `startOfWeek` ```kex startOfWeek : Date ``` The Monday of this date's week. The week runs Monday to Sunday, matching the ISO weekday numbering `weekday.number` reports. **Returns**: the Monday of this week **Examples** ```kex Date.of(2026, 7, 30).try.startOfWeek.iso # => "2026-07-27" ``` #### `endOfWeek` ```kex endOfWeek : Date ``` The Sunday of this date's week. **Returns**: the Sunday of this week **Examples** ```kex Date.of(2026, 7, 30).try.endOfWeek.iso # => "2026-08-02" ``` #### `daysUntil` ```kex daysUntil(other: Date) -> Integer ``` Whole days from this date to `other`, negative when `other` is earlier. **Parameters** - `other` — the date to measure to **Returns**: the number of days **Examples** ```kex Date.of(2026, 7, 30).try.daysUntil(Date.of(2026, 8, 9).try) # => 10 Date.of(2026, 8, 9).try.daysUntil(Date.of(2026, 7, 30).try) # => -10 ``` #### `until` ```kex until(other: Date) -> Duration ``` The span from this date to `other`, as a `Duration` of whole days. **Parameters** - `other` — the date to measure to **Returns**: the elapsed span **Examples** ```kex Date.of(2026, 7, 30).try.until(Date.of(2026, 8, 9).try).wholeDays # => 10 ``` #### `monthsUntil` ```kex monthsUntil(other: Date) -> Integer ``` Whole calendar months from this date to `other`, negative when `other` is earlier. Truncated, not rounded: a partial month does not count, so January 15th to February 14th is 0 months. The count is the exact inverse of `addMonths`, which is why the correction below asks `addMonths` rather than comparing day-of-month fields: January 31st plus one month IS February 28th, so January 31st to February 28th is one month, even though 28 < 31. Comparing the day fields answers 0 there and contradicts the addition this same file performs. **Parameters** - `other` — the date to measure to **Returns**: the number of whole months **Examples** ```kex Date.of(2026, 1, 15).try.monthsUntil(Date.of(2026, 2, 14).try) # => 0 Date.of(2026, 1, 31).try.monthsUntil(Date.of(2026, 2, 28).try) # => 1 ``` #### `yearsUntil` ```kex yearsUntil(other: Date) -> Integer ``` Whole calendar years from this date to `other`, negative when `other` is earlier. Truncated, like `monthsUntil`. This is how to compute an age. **Parameters** - `other` — the date to measure to **Returns**: the number of whole years **Examples** ```kex Date.of(2020, 1, 1).try.yearsUntil(Date.of(2026, 7, 30).try) # => 6 ``` _Someone's age today_ ```kex born.yearsUntil(Date.today()) ``` #### `before?` ```kex before?(other: Date) -> Bool ``` Returns `true` when this date is earlier than `other`. **Parameters** - `other` — the date to compare against **Returns**: `true` when this date comes first **Examples** ```kex Date.of(2026, 7, 30).try.before?(Date.of(2026, 8, 1).try) # => true ``` #### `after?` ```kex after?(other: Date) -> Bool ``` Returns `true` when this date is later than `other`. **Parameters** - `other` — the date to compare against **Returns**: `true` when this date comes second **Examples** ```kex Date.of(2026, 8, 1).try.after?(Date.of(2026, 7, 30).try) # => true ``` #### `compareTo` ```kex compareTo(other: Date) -> Ordering ``` Orders this date against another. **Parameters** - `other` — the date to compare against **Returns**: `Less`, `Equal` or `Greater` **Examples** ```kex Date.of(2026, 7, 30).try.compareTo(Date.of(2026, 8, 1).try) # => Less ``` _Sorting dates_ ```kex dates.sort { |a, b| a.compareTo(b) == Less } ``` #### `iso` ```kex iso : String ``` The date as ISO 8601 text, `2026-07-30`. **Returns**: the ISO 8601 date **Examples** ```kex Date.of(2026, 7, 30).try.iso # => "2026-07-30" ``` #### `at` ```kex at(time: Time, offset: Duration) -> DateTime ``` This date at a given time of day and offset, as a `DateTime`. **Parameters** - `time` — the time of day - `offset` — the offset from UTC **Returns**: the instant **Examples** ```kex Date.of(2026, 7, 30).try.at(Time.of(9, 0, 0).try, Duration.zero()).iso # => "2026-07-30T09:00:00Z" ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## record `Time` A time of day, with no date and no zone. ```kex let t = Time.of(14, 3, 0).try t.iso # => "14:03:00" (t + 2.hours).iso # => "16:03:00" ``` Arithmetic wraps within the day: there is no date to carry into. Reach for `DateTime` when the day rolling over matters. **Fields** - `hour` : [Integer](number.md#make-integer) - `minute` : [Integer](number.md#make-integer) - `second` : [Integer](number.md#make-integer) - `nanosecond` : [Integer](number.md#make-integer) Implements [`Inspectable`](kex.md#trait-inspectable), [`Showable`](kex.md#trait-showable). ### Functions #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Renders the time structurally, for debugging output. **Parameters** - `colors` — whether to include ANSI color escapes **Returns**: the rendered time #### `showValue` (from Showable) ```kex showValue : String ``` Renders the time in ISO form, so interpolation and `IO.printLine` show `14:03:00`. **Returns**: the ISO 8601 time **Examples** ```kex "starts at ${Time.of(14, 3, 0).try}" # => "starts at 14:03:00" ``` #### `secondsSinceMidnight` ```kex secondsSinceMidnight : Integer ``` Seconds from midnight to this time of day, ignoring the nanosecond field. **Returns**: seconds since midnight **Examples** ```kex Time.of(14, 3, 0).try.secondsSinceMidnight # => 50580 ``` #### `before?` ```kex before?(other: Time) -> Bool ``` Returns `true` when this time of day is earlier than `other`. **Parameters** - `other` — the time to compare against **Returns**: `true` when this time comes first **Examples** ```kex Time.of(14, 3, 0).try.before?(Time.of(15, 0, 0).try) # => true ``` #### `after?` ```kex after?(other: Time) -> Bool ``` Returns `true` when this time of day is later than `other`. **Parameters** - `other` — the time to compare against **Returns**: `true` when this time comes second **Examples** ```kex Time.of(15, 0, 0).try.after?(Time.of(14, 3, 0).try) # => true ``` #### `+` ```kex +(span: Duration) -> Time ``` Advances the time of day by a span, wrapping within the day. A Time has no date to carry into, so 23:00 + 2.hours is 01:00. Reach for `DateTime` when the day rolling over is something you need to see. The nanosecond field rides along untouched: `wholeSeconds` truncates the span, so a sub-second Duration shifts nothing. **Parameters** - `span` — the elapsed span to add **Returns**: the later time of day **Examples** ```kex (Time.of(14, 3, 0).try + 2.hours).iso # => "16:03:00" (Time.of(23, 0, 0).try + 2.hours).iso # => "01:00:00" ``` #### `-` ```kex -(span: Duration) -> Time ``` Moves the time of day back by a span, wrapping within the day. **Parameters** - `span` — the elapsed span to subtract **Returns**: the earlier time of day **Examples** ```kex (Time.midnight() - 1.seconds).iso # => "23:59:59" ``` #### `addSeconds` ```kex addSeconds(count: Integer) -> Time ``` The time of day `count` seconds later, wrapping within the day. **Parameters** - `count` — how many seconds to add **Returns**: the shifted time of day **Examples** ```kex Time.of(14, 3, 0).try.addSeconds(60).iso # => "14:04:00" ``` #### `addMinutes` ```kex addMinutes(count: Integer) -> Time ``` The time of day `count` minutes later, wrapping within the day. **Parameters** - `count` — how many minutes to add **Returns**: the shifted time of day **Examples** ```kex Time.of(14, 3, 0).try.addMinutes(30).iso # => "14:33:00" ``` #### `addHours` ```kex addHours(count: Integer) -> Time ``` The time of day `count` hours later, wrapping within the day. **Parameters** - `count` — how many hours to add **Returns**: the shifted time of day **Examples** ```kex Time.of(23, 0, 0).try.addHours(2).iso # => "01:00:00" ``` #### `until` ```kex until(other: Time) -> Duration ``` Elapsed time from this time of day to `other`, within the same day. Negative when `other` is earlier. Sub-second precision is kept. **Parameters** - `other` — the time to measure to **Returns**: the elapsed span **Examples** ```kex Time.of(14, 3, 0).try.until(Time.of(15, 3, 0).try).wholeMinutes # => 60 ``` #### `iso` ```kex iso : String ``` The time of day as ISO 8601 text, with fractional seconds only when they are non-zero. **Returns**: the ISO 8601 time **Examples** ```kex Time.of(14, 3, 0).try.iso # => "14:03:00" Time.of(14, 3, 0, 123456789).try.iso # => "14:03:00.123456789" ``` #### `compareTo` ```kex compareTo(other: Time) -> Ordering ``` Orders this time of day against another, nanoseconds included. **Parameters** - `other` — the time to compare against **Returns**: `Less`, `Equal` or `Greater` **Examples** ```kex Time.of(14, 3, 0).try.compareTo(Time.of(15, 0, 0).try) # => Less ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## record `DateTime` An instant: a calendar date, a time of day, and a fixed offset from UTC. ```kex let m = DateTime.parse("2026-07-30T14:03:00+02:00").try m.iso # => "2026-07-30T14:03:00+02:00" m.utc.iso # => "2026-07-30T12:03:00Z" ``` Two `DateTime` values that name the same instant compare equal whatever offsets they are written at: comparison goes through `epochSeconds`. **Fields** - `date` : [Date](#record-date) - `time` : [Time](#record-time) - `offset` : [Duration](units.md#record-duration) Implements [`Inspectable`](kex.md#trait-inspectable), [`Showable`](kex.md#trait-showable). ### Functions #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Renders the instant structurally, for debugging output. **Parameters** - `colors` — whether to include ANSI color escapes **Returns**: the rendered instant #### `showValue` (from Showable) ```kex showValue : String ``` Renders the instant in ISO form, so interpolation and `IO.printLine` show +2026-07-30T14:03:00+02:00+. **Returns**: the ISO 8601 instant **Examples** ```kex "logged at ${DateTime.utcNow()}" # => "logged at 2026-07-30T12:03:00Z" ``` #### `epochSeconds` ```kex epochSeconds : Integer ``` Seconds since the Unix epoch. This is the value to compare and subtract: it identifies the instant, independent of the offset rendering it, so 12:00Z and 14:00+02:00 have the same one. **Returns**: seconds since the Unix epoch **Examples** ```kex DateTime.parse("1970-01-01T00:00:00Z").try.epochSeconds # => 0 ``` _Storing an instant as a number_ ```kex FS.File.write("stamp", "${DateTime.utcNow().epochSeconds}") ``` #### `weekday` ```kex weekday : Weekday ``` The day of the week this instant falls on, at its own offset. No `year`/`hour`/... shorthands here: a function named after a record field makes `value.year` inside a module function dispatch to it on BEAM and fail with function_clause. Reach through `.date` and `.time`. **Returns**: the day of the week **Examples** ```kex DateTime.parse("2026-07-30T14:03:00Z").try.weekday.name # => "Thursday" ``` #### `at` ```kex at(offset: Duration) -> DateTime ``` The same instant, rendered at another offset. Nothing moves: the wall clock changes because the offset does, and `epochSeconds` is unchanged. **Parameters** - `offset` — the offset to render at **Returns**: the same instant, at that offset **Examples** ```kex DateTime.parse("2026-07-30T14:03:00+02:00").try .at(Duration.utcOffset(-5, 0)).iso # => "2026-07-30T07:03:00-05:00" ``` #### `utc` ```kex utc : DateTime ``` The same instant, rendered at UTC. The form to store, compare and transmit: there is no offset to disagree about. **Returns**: the same instant, at UTC **Examples** ```kex DateTime.parse("2026-07-30T14:03:00+02:00").try.utc.iso # => "2026-07-30T12:03:00Z" ``` #### `+` ```kex +(span: Duration) -> DateTime ``` Advances the instant by a fixed span, keeping its offset. **Parameters** - `span` — the elapsed span to add **Returns**: the later instant **Examples** ```kex (DateTime.parse("2026-07-30T14:03:00Z").try + 90.minutes).iso # => "2026-07-30T15:33:00Z" ``` #### `-` ```kex -(span: Duration) -> DateTime ``` Moves the instant back by a fixed span, keeping its offset. **Parameters** - `span` — the elapsed span to subtract **Returns**: the earlier instant **Examples** ```kex (DateTime.utcNow() - 24.hours).iso # yesterday, same wall clock ``` #### `addDays` ```kex addDays(count: Integer) -> DateTime ``` The instant `count` days later, keeping the wall clock and the offset. **Parameters** - `count` — how many days to add **Returns**: the shifted instant **Examples** ```kex DateTime.parse("2026-07-30T14:03:00Z").try.addDays(1).iso # => "2026-07-31T14:03:00Z" ``` #### `addWeeks` ```kex addWeeks(count: Integer) -> DateTime ``` The instant `count` weeks later, keeping the wall clock and the offset. **Parameters** - `count` — how many weeks to add **Returns**: the shifted instant **Examples** ```kex DateTime.parse("2026-07-30T14:03:00Z").try.addWeeks(1).iso # => "2026-08-06T14:03:00Z" ``` #### `addMonths` ```kex addMonths(count: Integer) -> DateTime ``` The instant `count` calendar months later, with the day clamped into the target month. **Parameters** - `count` — how many months to add **Returns**: the shifted instant **Examples** ```kex DateTime.parse("2026-01-31T09:00:00Z").try.addMonths(1).iso # => "2026-02-28T09:00:00Z" ``` #### `addYears` ```kex addYears(count: Integer) -> DateTime ``` The instant `count` calendar years later, with the day clamped. **Parameters** - `count` — how many years to add **Returns**: the shifted instant **Examples** ```kex DateTime.parse("2024-02-29T09:00:00Z").try.addYears(1).iso # => "2025-02-28T09:00:00Z" ``` #### `until` ```kex until(other: DateTime) -> Duration ``` Elapsed time from this instant to `other`, negative when `other` is earlier. Sub-second precision is kept. **Parameters** - `other` — the instant to measure to **Returns**: the elapsed span **Examples** ```kex started.until(DateTime.utcNow()).wholeSeconds ``` _Enforcing a deadline_ ```kex if started.until(DateTime.utcNow()).longerThan?(30.seconds) IO.printError("timed out") end ``` #### `before?` ```kex before?(other: DateTime) -> Bool ``` Returns `true` when this instant is earlier than `other`, whatever offsets they are written at. **Parameters** - `other` — the instant to compare against **Returns**: `true` when this instant comes first **Examples** ```kex DateTime.parse("2026-07-30T14:03:00+02:00").try .before?(DateTime.parse("2026-07-31T00:00:00Z").try) # => true ``` #### `after?` ```kex after?(other: DateTime) -> Bool ``` Returns `true` when this instant is later than `other`. **Parameters** - `other` — the instant to compare against **Returns**: `true` when this instant comes second **Examples** ```kex DateTime.utcNow().after?(started) # => true ``` #### `iso` ```kex iso : String ``` The instant as ISO 8601 text, +2026-07-30T14:03:00+02:00+. **Returns**: the ISO 8601 instant **Examples** ```kex DateTime.parse("2026-07-30T14:03:00+02:00").try.iso # => "2026-07-30T14:03:00+02:00" ``` #### `compareTo` ```kex compareTo(other: DateTime) -> Ordering ``` Orders this instant against another, by instant rather than by wall clock, so 12:00Z and 14:00+02:00 compare `Equal`. Named `compareTo` rather than `compare`: a make-block `compare` is shadowed by the builtin comparison dispatch and fails at runtime on both backends. **Parameters** - `other` — the instant to compare against **Returns**: `Less`, `Equal` or `Greater` **Examples** ```kex DateTime.parse("2026-07-30T12:00:00Z").try .compareTo(DateTime.parse("2026-07-30T14:00:00+02:00").try) # => Equal ``` _Sorting events by when they happened_ ```kex events.sort { |a, b| a.at.compareTo(b.at) == Less } ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## record `Period` A calendar span. Months and years have no fixed length: February is 28 days or 29, a year 365 or 366, so they cannot live in a `Duration`, which is a count of seconds and nothing else. A Period carries the calendar fields themselves and lets the calendar resolve them: ```kex Date.of(2026, 1, 31).try + 1.months # 2026-02-28, not 2026-03-03 Date.of(2024, 2, 29).try + 1.years # 2025-02-28 ``` Use a Duration for elapsed time (`36.hours` is always 129600 seconds) and a Period for calendar steps (`1.months` is however long that month is). **Fields** - `years` : [Integer](number.md#make-integer) - `months` : [Integer](number.md#make-integer) - `days` : [Integer](number.md#make-integer) Implements [`Inspectable`](kex.md#trait-inspectable), [`Showable`](kex.md#trait-showable). ### Functions #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Renders the period structurally, for debugging output. **Parameters** - `colors` — whether to include ANSI color escapes **Returns**: the rendered period #### `showValue` (from Showable) ```kex showValue : String ``` Renders the period as its ISO 8601 form, so interpolation and `IO.printLine` show `P1Y2M3D`. **Returns**: the ISO 8601 duration **Examples** ```kex "due in ${Period.of(0, 1, 0)}" # => "due in P1M" ``` #### `+` ```kex +(other: Period) -> Period ``` Adds two calendar spans, field by field. **Parameters** - `other` — the span to add **Returns**: the total **Examples** ```kex (Period.of(1, 2, 3) + 1.years).iso # => "P2Y2M3D" ``` #### `-` ```kex -(other: Period) -> Period ``` Subtracts a calendar span, field by field. Fields may go negative. **Parameters** - `other` — the span to subtract **Returns**: the difference **Examples** ```kex (Period.of(1, 2, 3) - 1.years).iso # => "P2M3D" ``` #### `negated` ```kex negated : Period ``` The same span with every field's sign flipped: a step backwards. **Returns**: the negated span **Examples** ```kex Period.of(1, 2, 3).negated.iso # => "P-1Y-2M-3D" ``` #### `*` ```kex *(factor: Integer) -> Period ``` Multiplies every field by `factor`. **Parameters** - `factor` — the factor to multiply by **Returns**: the scaled span **Examples** ```kex (Period.of(1, 2, 3) * 2).iso # => "P2Y4M6D" ``` #### `zero?` ```kex zero? : Bool ``` Returns `true` when every field is zero. **Returns**: `true` for a zero span **Examples** ```kex Period.zero().zero? # => true Period.of(0, 1, 0).zero? # => false ``` #### `totalMonths` ```kex totalMonths : Integer ``` The years and months of the span, as one count of months. Years and months are one quantity for arithmetic: a month is 1/12 of a year exactly, unlike days, which are not a fixed fraction of a month. This is why `date + span` applies them together rather than one after the other. **Returns**: the total months, ignoring days **Examples** ```kex Period.of(1, 2, 3).totalMonths # => 14 ``` #### `normalized` ```kex normalized : Period ``` The same span with excess months carried into years. 14 months becomes 1 year 2 months. Days are left alone: there is no fixed number of them in a month to carry by. **Returns**: the normalised span **Examples** ```kex 14.months.normalized.iso # => "P1Y2M" ``` #### `iso` ```kex iso : String ``` The span as an ISO 8601 duration, `P1Y2M3D`. A zero period is `P0D`, the shortest spelling the grammar allows: `P` alone is not valid. **Returns**: the ISO 8601 duration **Examples** ```kex Period.of(1, 2, 3).iso # => "P1Y2M3D" Period.zero().iso # => "P0D" 2.months.iso # => "P2M" ``` ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## module `Time` Building times of day, controlling the clock in tests, and the calendar arithmetic the rest of this file is written on. ### `of` ```kex of(hour: Integer, minute: Integer, second: Integer, nanosecond: Integer) -> Result ``` Builds a validated time of day. Every field is range-checked, so a `Time` you hold is always a real time. Leap seconds are not modeled, so a second of 60 is rejected. **Parameters** - `hour` — the hour, 0 through 23 - `minute` — the minute, 0 through 59 - `second` — the second, 0 through 59 - `nanosecond` — the sub-second part, 0 through 999999999; omitted for 0 **Returns**: the time, or why it is not one **Examples** ```kex Time.of(14, 3, 0).map { |t| t.iso } # => Ok("14:03:00") Time.of(25, 0, 0) # => Error(InvalidTime(25, 0, 0)) ``` _With sub-second precision_ ```kex Time.of(14, 3, 0, 123456789).map { |t| t.iso } # => Ok("14:03:00.123456789") ``` ### `midnight` ```kex midnight : Time ``` Midnight, 00:00:00. The start of a day. **Returns**: 00:00:00 **Examples** ```kex Time.midnight().iso # => "00:00:00" (Time.midnight() - 1.seconds).iso # => "23:59:59" ``` ### `fromSecondsSinceMidnight` ```kex fromSecondsSinceMidnight(count: Integer, nanosecond: Integer) -> Time ``` Builds a time of day from a count of seconds since midnight. Wraps, so 86400 is midnight again and -1 is 23:59:59, which is what makes it total where `Time.of` is fallible. Declared before the two-argument form: the interpreter resolves an overloaded module function to its LAST definition regardless of arity, so a delegating overload has to come first or it recurses into itself. **Parameters** - `count` — seconds since midnight; wraps outside 0..86399 - `nanosecond` — the sub-second part; omitted for 0 **Returns**: the time of day **Examples** ```kex Time.fromSecondsSinceMidnight(50580).iso # => "14:03:00" Time.fromSecondsSinceMidnight(86400).iso # => "00:00:00" Time.fromSecondsSinceMidnight(-1).iso # => "23:59:59" ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses an ISO 8601 time of day. Accepts `14:03`, `14:03:00`, or `14:03:00.123456789`. Anything else is `InvalidFormat`. **Parameters** - `text` — the text to parse **Returns**: the time, or why it could not be read **Examples** ```kex Time.parse("14:03").map { |t| t.iso } # => Ok("14:03:00") Time.parse("xx") # => Error(InvalidFormat("xx")) ``` ### `now` ```kex now : Time ``` The current time of day, in this machine's zone. Reads `Time.Clock` like everything else here does, so it is pinned by replacing that capability in a test. **Returns**: the current time of day **Examples** ```kex Time.now().iso # => "14:03:00" ``` ### `utcNow` ```kex utcNow : Time ``` The current time of day in UTC, whatever this machine's zone is. **Returns**: the current UTC time of day **Examples** ```kex Time.utcNow().iso # => "12:03:00" ``` ### `parseOffset` ```kex parseOffset(text: String) -> Result ``` Parses an ISO 8601 zone designator into an offset. Accepts `Z`, +`02:00`, `-05:30`, or the empty string (all meaning UTC for the first and last). **Parameters** - `text` — the zone designator **Returns**: the offset, or why it could not be read **Examples** ```kex Time.parseOffset("+02:00").map { |d| d.wholeSeconds } # => Ok(7200) Time.parseOffset("Z").map { |d| d.wholeSeconds } # => Ok(0) ``` ### `momentAt` ```kex momentAt(nanos: Integer) -> DateTime ``` The civil instant `nanos` after the Unix epoch, in this machine's zone as it stands at that instant. Foul because the zone is ambient. **Parameters** - `nanos` — nanoseconds since the Unix epoch **Returns**: that instant, in the local zone ### `nanosOf` ```kex nanosOf(moment: DateTime) -> Integer ``` Nanoseconds since the Unix epoch for a civil datetime. A plain function rather than a `DateTime` function: on BEAM a function named `epochNanos` flattens onto the same name as the `DateTime.epochNanos()` module function, and the arity-0 one wins: silently, answering for the host clock instead of for `moment`. ### `leapYear?` ```kex leapYear?(year: Integer) -> Bool ``` Public because the `make` blocks below live at file level and reach them by qualification; they are equally useful on their own. Returns `true` when `year` is a leap year in the proleptic Gregorian calendar. **Parameters** - `year` — the calendar year **Returns**: `true` for a leap year **Examples** ```kex Time.leapYear?(2024) # => true Time.leapYear?(1900) # => false Time.leapYear?(2000) # => true ``` ### `daysInMonth` ```kex daysInMonth(year: Integer, month: Integer) -> Result ``` The number of days in a month. Year first, matching `Date.of(year, month, day)` and every other date-shaped signature in this file. Returns `Error(InvalidDate(year, month, 1))` when the month is outside 1 through 12. **Parameters** - `year` — the calendar year, which decides February's length - `month` — the month, 1 through 12 **Returns**: the day count, or why the month is not one **Examples** ```kex Time.daysInMonth(2024, 2) # => Ok(29) Time.daysInMonth(2023, 2) # => Ok(28) Time.daysInMonth(2026, 13) # => Error(InvalidDate(2026, 13, 1)) ``` ### `daysInValidMonth` ```kex daysInValidMonth(year: Integer, month: Integer) -> Integer ``` The number of days in a month, with the range check already done. Every caller inside this file has a month it built or validated itself. Use `daysInMonth` for a month that came from outside. **Parameters** - `year` — the calendar year - `month` — the month, assumed to be 1 through 12 **Returns**: the day count **Examples** ```kex Time.daysInValidMonth(2024, 2) # => 29 ``` ### `daysFromCivil` ```kex daysFromCivil(year: Integer, month: Integer, day: Integer) -> Integer ``` The number of days from 1970-01-01 to a calendar date, negative before it. Howard Hinnant's civil-calendar algorithms: exact across the whole proleptic Gregorian range, and they need only truncating integer division: the semantics Kex's `/` already has. **Parameters** - `year` — the calendar year - `month` — the month, 1 through 12 - `day` — the day of the month **Returns**: days since the Unix epoch **Examples** ```kex Time.daysFromCivil(1970, 1, 1) # => 0 Time.daysFromCivil(2026, 7, 30) # => 20664 ``` ### `civilFromDays` ```kex civilFromDays(epochDay: Integer) -> Date ``` The calendar date a count of days since 1970-01-01 lands on. The inverse of `daysFromCivil`. **Parameters** - `epochDay` — days since the Unix epoch **Returns**: the calendar date **Examples** ```kex Time.civilFromDays(0).iso # => "1970-01-01" Time.civilFromDays(20664).iso # => "2026-07-30" ``` ### `weekdayFromEpochDay` ```kex weekdayFromEpochDay(epochDay: Integer) -> Weekday ``` The weekday a count of days since 1970-01-01 falls on. **Parameters** - `epochDay` — days since the Unix epoch **Returns**: the day of the week **Examples** ```kex Time.weekdayFromEpochDay(0) # => Thursday (1970-01-01 was a Thursday) ``` ### `weekdayNumber` ```kex weekdayNumber(weekday: Weekday) -> Integer ``` The ISO number of a weekday: Monday is 1, Sunday is 7. `weekday.number` is the readable way to ask. **Parameters** - `weekday` — the day of the week **Returns**: its ISO number, 1 through 7 **Examples** ```kex Time.weekdayNumber(Monday) # => 1 ``` ### `weekdayName` ```kex weekdayName(weekday: Weekday) -> String ``` The English name of a weekday. `weekday.name` is the readable way to ask. **Parameters** - `weekday` — the day of the week **Returns**: its English name **Examples** ```kex Time.weekdayName(Sunday) # => "Sunday" ``` ### `errorMessage` ```kex errorMessage(error: TimeError) -> String ``` Renders a `TimeError` as a sentence for the user. A plain function rather than an `Errorable` implementation: a `message` function here joins the same BEAM dispatcher as ParseError's `message` FIELD and breaks it (spec/record_field_method_collision.kex). **Parameters** - `error` — the failure to describe **Returns**: the message to show **Examples** ```kex Time.errorMessage(InvalidDate(2026, 13, 1)) # => "not a calendar date: 2026-13-1" ``` _Reporting a rejected date_ ```kex match Date.parse(input) do Ok(date) => IO.printLine(date.iso) Error(e) => IO.printError(Time.errorMessage(e)) end ``` ### `formatDate` ```kex formatDate(value: Date) -> String ``` ### `formatDateTime` ```kex formatDateTime(value: DateTime) -> String ``` ### `formatTime` ```kex formatTime(value: Time) -> String ``` ### `formatFraction` ```kex formatFraction(nanosecond: Integer) -> String ``` Fractional seconds, in the 3/6/9-digit groupings ISO 8601 output conventionally uses, whichever is the shortest that loses nothing. A whole second renders no fraction at all, so `14:03:00` is unchanged. Without this the nanosecond field was kept on the value and compared, but never rendered: `Time.parse("14:03:00.5")` and `Time.parse("14:03:00")` produced different values that printed identically, and every parse/format round-trip silently dropped sub-second precision. ### `formatOffset` ```kex formatOffset(offset: Duration) -> String ``` ±HH:MM, the shape an ISO 8601 offset takes. UTC renders as "Z". ### `withNanosecond` ```kex withNanosecond(moment: DateTime, nanosecond: Integer) -> DateTime ``` ### `floorDiv` ```kex floorDiv(value: Integer, divisor: Integer) -> Integer ``` Kex's `/` truncates toward zero; instants before the epoch need the floor. ### `truncatedBy` ```kex truncatedBy(seconds: Float, unit: Float) -> Integer ``` ### `pad2` ```kex pad2(value: Integer) -> String ``` ### `padTo` ```kex padTo(value: Integer, width: Integer) -> String ``` Left-pad with zeros to a fixed width. A value already that wide is left alone rather than truncated: losing digits would be worse than a field one character too long. ### `padYear` ```kex padYear(value: Integer) -> String ``` Years keep four digits where they fit; ISO 8601 has no fixed spelling beyond that, so wider years render as-is. ### `digitsIn` ```kex digitsIn(fragment: String, whole: String) -> Result ``` Digits, with the failure reported against the WHOLE input rather than the fragment that failed: `Time.parse("2:03 pm")` should complain about "2:03 pm", not about "03 pm". ### `digitsToInteger` ```kex digitsToInteger(text: String) -> Result ``` ### `parseFraction` ```kex parseFraction(text: String, whole: String) -> Result ``` ".5" is 500000000ns: the digits are padded out to nanosecond scale. ### `splitOffset` ```kex splitOffset(text: String) -> [String] ``` Splits "14:03:00+02:00" into its time and offset halves. A missing offset reads as UTC, matching what a zero offset formats back to. ## module `Time.Clock` The clock: the one place the current instant comes from. Anything that asks what time it is (`Time.now`, `Date.today`, `DateTime.utcNow`) reads this, so replacing it replaces the whole calendar. It is a capability, so the replacement is for a lexical region: it reaches every call made from the region and every process spawned in it, it nests, and it cannot outlive its block. This is what makes code that calls `Date.today()` testable: ```kex with Time.Clock = Mock.Clock { at: DateTime.parse("2026-07-30T14:03:00Z").try } do Date.today().iso # "2026-07-30", always end ``` `now` is the whole interface. Its answer carries the zone as its offset, so one member covers both ambient inputs: what time it is, and where. `Mock.Clock` is the ready-made stand-in; any record that implements `now` is one too. ### `now` ```kex now : DateTime ``` The current instant, in this machine's zone as it stands right now. **Returns**: the current instant ## type `Weekday` ### `number` ```kex number : Integer ``` This weekday's ISO number: Monday is 1, Sunday is 7. **Returns**: the ISO number, 1 through 7 **Examples** ```kex Monday.number # => 1 Sunday.number # => 7 ``` ### `name` ```kex name : String ``` This weekday's English name. **Returns**: the name **Examples** ```kex Date.of(2026, 7, 30).try.weekday.name # => "Thursday" ``` ### `weekend?` ```kex weekend? : Bool ``` Returns `true` for Saturday and Sunday. **Returns**: `true` on a weekend day **Examples** ```kex Saturday.weekend? # => true Monday.weekend? # => false ``` _Counting working days in a range_ ```kex days.count { |d| !d.weekday.weekend? } ``` ## module `Date` Building calendar dates, and asking what today is. ### `of` ```kex of(year: Integer, month: Integer, day: Integer) -> Result ``` Builds a validated calendar date. The month and the day are both range-checked, and the day is checked against that month's actual length, so February 30th is an `Error`, and a `Date` you hold is always a real day. The record literal `Date { ... }` bypasses this, so prefer it for anything derived from input. **Parameters** - `year` — the calendar year - `month` — the month, 1 through 12 - `day` — the day of the month **Returns**: the date, or why it is not one **Examples** ```kex Date.of(2026, 7, 30).map { |d| d.iso } # => Ok("2026-07-30") Date.of(2026, 13, 1) # => Error(InvalidDate(2026, 13, 1)) Date.of(2023, 2, 29) # => Error(InvalidDate(2023, 2, 29)) ``` _Taking the date or failing loudly_ ```kex let due = Date.of(year, month, day).try ``` ### `fromEpochDay` ```kex fromEpochDay(day: Integer) -> Date ``` The calendar date a count of days since 1970-01-01 lands on, negative before it. **Parameters** - `day` — days since the Unix epoch **Returns**: the calendar date **Examples** ```kex Date.fromEpochDay(0).iso # => "1970-01-01" Date.fromEpochDay(20664).iso # => "2026-07-30" ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses an ISO 8601 calendar date, `2026-07-30`. The result is validated as well as parsed, so a well-formed but impossible date is `InvalidDate` rather than `InvalidFormat`. **Parameters** - `text` — the text to parse **Returns**: the date, or why it could not be read **Examples** ```kex Date.parse("2026-07-30").map { |d| d.iso } # => Ok("2026-07-30") Date.parse("nope") # => Error(InvalidFormat("nope")) Date.parse("2026-02-30") # => Error(InvalidDate(2026, 2, 30)) ``` ### `now` ```kex now : Date ``` Today's date, in this machine's zone. `Date.today()` reads better in most code; `now` exists so every type in this file answers the same question the same way. **Returns**: today's date **Examples** ```kex Date.now().iso # => "2026-07-30" ``` ### `today` ```kex today : Date ``` Today's date, in this machine's zone. Pinned by replacing `Time.Clock` in a test, like everything else that reads the clock. **Returns**: today's date **Examples** ```kex Date.today().iso # => "2026-07-30" ``` _Testing code that depends on today_ ```kex with Time.Clock = Mock.Clock { at: DateTime.parse("2026-07-30T00:00:00Z").try } do Assert.equal(Date.today().iso, "2026-07-30") end ``` ### `tomorrow` ```kex tomorrow : Date ``` The day after today, in this machine's zone. **Returns**: tomorrow's date **Examples** ```kex Date.tomorrow().iso # => "2026-07-31" ``` ### `yesterday` ```kex yesterday : Date ``` The day before today, in this machine's zone. **Returns**: yesterday's date **Examples** ```kex Date.yesterday().iso # => "2026-07-29" ``` ### `utcNow` ```kex utcNow : Date ``` Today's date in UTC, whatever this machine's zone is. **Returns**: today's UTC date **Examples** ```kex Date.utcNow().iso # => "2026-07-30" ``` ### `utcToday` ```kex utcToday : Date ``` Today's date in UTC. The same as `Date.utcNow()`, under the name that reads better. **Returns**: today's UTC date **Examples** ```kex Date.utcToday().iso # => "2026-07-30" ``` ## module `DateTime` Building instants, and asking what time it is now. ### `of` ```kex of(date: Date, time: Time, offset: Duration) -> DateTime ``` Combines a date, a time of day and a UTC offset into an instant. **Parameters** - `date` — the calendar date - `time` — the time of day - `offset` — the offset from UTC **Returns**: the instant **Examples** ```kex DateTime.of(Date.of(2026, 7, 30).try, Time.of(9, 0, 0).try, Duration.zero()).iso # => "2026-07-30T09:00:00Z" ``` _At an offset_ ```kex DateTime.of(date, time, Duration.utcOffset(2, 0)) ``` ### `fromEpochSeconds` ```kex fromEpochSeconds(count: Integer, offset: Duration) -> DateTime ``` The instant a count of seconds since the Unix epoch names, rendered at UTC or at the offset you give. Declared before the two-argument form: the interpreter resolves an overloaded module function to its LAST definition regardless of arity, so a delegating overload has to come first or it recurses into itself. **Parameters** - `count` — seconds since the Unix epoch - `offset` — the offset to render at; omitted for UTC **Returns**: the instant **Examples** ```kex DateTime.fromEpochSeconds(0).iso # => "1970-01-01T00:00:00Z" ``` _Rendered at a local offset_ ```kex DateTime.fromEpochSeconds(0, Duration.utcOffset(2, 0)).iso # => "1970-01-01T02:00:00+02:00" ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses an ISO 8601 instant. Accepts +2026-07-30T14:03:00+02:00+, the same with `Z`, or a bare civil datetime with no zone at all, which is read as UTC. **Parameters** - `text` — the text to parse **Returns**: the instant, or why it could not be read **Examples** ```kex DateTime.parse("2026-07-30T14:03:00+02:00").map { |m| m.utc.iso } # => Ok("2026-07-30T12:03:00Z") DateTime.parse("nope") # => Error(InvalidFormat("nope")) ``` _Reading a timestamp out of a log line_ ```kex DateTime.parse(line.take(20)).map { |m| m.epochSeconds } ``` ### `now` ```kex now : DateTime ``` The current instant, in this machine's zone as it stands right now. The offset is the one in effect at this instant, so it is right today. Named IANA zones are not modeled, so it cannot say what the offset WILL be for some future local time. **Returns**: the current instant **Examples** ```kex DateTime.now().iso # => "2026-07-30T14:03:00+02:00" ``` ### `utcNow` ```kex utcNow : DateTime ``` The current instant, at UTC. The form to prefer when the value is stored, compared or transmitted: there is no zone to disagree about. **Returns**: the current instant, at UTC **Examples** ```kex DateTime.utcNow().iso # => "2026-07-30T12:03:00Z" ``` ### `epochNanos` ```kex epochNanos : Integer ``` Nanoseconds since the Unix epoch, straight from the clock. The right reading for measuring a short interval: a plain integer, with no zone to disagree about. **Returns**: nanoseconds since the Unix epoch **Examples** _Timing a piece of work_ ```kex let started = DateTime.epochNanos() doTheThing() let elapsedMs = (DateTime.epochNanos() - started) / 1000000 ``` ## extends `Integer` More functions for [`Integer`](number.md#make-integer), added by this module. The plural spellings build a Duration; the singular ones from units.kex build a time Measure. `5.seconds` is an elapsed span, `5.second` a measurement. ### `nanoseconds` ```kex nanoseconds : Duration ``` ### `microseconds` ```kex microseconds : Duration ``` ### `milliseconds` ```kex milliseconds : Duration ``` This many milliseconds, as a `Duration`. **Returns**: the elapsed span **Examples** ```kex 500.milliseconds.wholeMilliseconds # => 500 ``` ### `seconds` ```kex seconds : Duration ``` This many seconds, as a `Duration`. Note the plural: `5.seconds` is an elapsed span, while `5.second` from `units.kex` is a measurement. **Returns**: the elapsed span **Examples** ```kex 30.seconds.wholeSeconds # => 30 ``` ### `minutes` ```kex minutes : Duration ``` This many minutes, as a `Duration`. **Returns**: the elapsed span **Examples** ```kex 90.minutes.wholeHours # => 1 ``` ### `hours` ```kex hours : Duration ``` This many hours, as a `Duration`. **Returns**: the elapsed span **Examples** ```kex 36.hours.wholeDays # => 1 ``` ### `days` ```kex days : Duration ``` This many days, as a `Duration`: a fixed 86400 seconds each. Use `Period.days` when the calendar should get a say. **Returns**: the elapsed span **Examples** ```kex (Date.of(2026, 7, 30).try + 10.days).iso # => "2026-08-09" ``` ### `weeks` ```kex weeks : Duration ``` This many weeks, as a `Duration`: a fixed 604800 seconds each. **Returns**: the elapsed span **Examples** ```kex 2.weeks.wholeDays # => 14 ``` ### `months` ```kex months : Period ``` This many calendar months, as a `Period`. A calendar span, not a Duration: see the `Period` record above. `4.weeks` and `1.months` are deliberately different things: the first is exactly 28 days, the second is one calendar month, however long that turns out to be. **Returns**: the calendar span **Examples** ```kex (Date.of(2026, 7, 30).try + 1.months).iso # => "2026-08-30" ``` _The calendar clamps a day that does not exist_ ```kex (Date.of(2026, 1, 31).try + 1.months).iso # => "2026-02-28" ``` ### `years` ```kex years : Period ``` This many calendar years, as a `Period`. **Returns**: the calendar span **Examples** ```kex (Date.of(2024, 2, 29).try + 1.years).iso # => "2025-02-28" ``` ## extends `Float` More functions for [`Float`](number.md#make-float), added by this module. The same `Duration` constructors on `Float`, for fractional spans: `1.5.hours`, `0.25.seconds`. ### `nanoseconds` ```kex nanoseconds : Duration ``` ### `microseconds` ```kex microseconds : Duration ``` ### `milliseconds` ```kex milliseconds : Duration ``` This many milliseconds, as a `Duration`. **Returns**: the elapsed span ### `seconds` ```kex seconds : Duration ``` This many seconds, as a `Duration`. **Returns**: the elapsed span **Examples** ```kex 0.25.seconds.wholeMilliseconds # => 250 ``` ### `minutes` ```kex minutes : Duration ``` This many minutes, as a `Duration`. **Returns**: the elapsed span ### `hours` ```kex hours : Duration ``` This many hours, as a `Duration`. **Returns**: the elapsed span **Examples** ```kex 1.5.hours.wholeMinutes # => 90 ``` ### `days` ```kex days : Duration ``` This many days, as a `Duration`: a fixed 86400 seconds each. **Returns**: the elapsed span ### `weeks` ```kex weeks : Duration ``` This many weeks, as a `Duration`: a fixed 604800 seconds each. **Returns**: the elapsed span ## extends `Duration` More functions for [`Duration`](units.md#record-duration), added by this module. ### `wholeNanoseconds` ```kex wholeNanoseconds : Integer ``` ### `wholeMicroseconds` ```kex wholeMicroseconds : Integer ``` ### `+` ```kex +(other: Duration) -> Duration ``` Adds two spans. **Parameters** - `other` — the span to add **Returns**: the total **Examples** ```kex (90.minutes + 30.minutes).wholeHours # => 2 ``` ### `-` ```kex -(other: Duration) -> Duration ``` Subtracts a span. The result may be negative. **Parameters** - `other` — the span to subtract **Returns**: the difference **Examples** ```kex (1.hours - 90.minutes).negative? # => true ``` ### `negated` ```kex negated : Duration ``` The same span with its sign flipped. **Returns**: the negated span **Examples** ```kex 90.minutes.negated.negative? # => true ``` ### `*` ```kex *(factor: Number) -> Duration ``` Multiplies the span by a plain number. `3 * 1.days` is not the same call because the receiver has to be the Duration, so it is spelled `1.days * 3`. **Parameters** - `factor` — the factor to multiply by **Returns**: the scaled span **Examples** ```kex (90.minutes * 2).wholeHours # => 3 ``` ### `/` ```kex /(divisor: Number) -> Duration ``` Divides the span by a plain number. **Parameters** - `divisor` — the number to divide by **Returns**: the scaled span **Examples** ```kex (90.minutes / 2).wholeMinutes # => 45 ``` ### `abs` ```kex abs : Duration ``` The span's magnitude, discarding its direction. **Returns**: the absolute span **Examples** ```kex (1.hours - 90.minutes).abs.wholeMinutes # => 30 ``` ### `zero?` ```kex zero? : Bool ``` Returns `true` when the span is exactly zero. **Returns**: `true` for a zero span **Examples** ```kex Duration.zero().zero? # => true ``` ### `negative?` ```kex negative? : Bool ``` Returns `true` when the span points backwards. A negative span is what `until` gives you when the other moment is earlier, so this is how to ask which came first. **Returns**: `true` for a negative span **Examples** ```kex date.until(other).negative? # => true when `other` is earlier ``` ### `positive?` ```kex positive? : Bool ``` Returns `true` when the span points forwards. **Returns**: `true` for a positive span **Examples** ```kex 90.minutes.positive? # => true ``` ### `shorterThan?` ```kex shorterThan?(other: Duration) -> Bool ``` Returns `true` when this span is shorter than `other`. Named for length rather than for order: `before?`/`after?` are about when something happened, and a Duration is not a point in time. **Parameters** - `other` — the span to compare against **Returns**: `true` when this span is shorter **Examples** ```kex 30.minutes.shorterThan?(1.hours) # => true ``` _Enforcing a timeout_ ```kex if started.until(DateTime.utcNow()).longerThan?(30.seconds) IO.printError("timed out") end ``` ### `longerThan?` ```kex longerThan?(other: Duration) -> Bool ``` Returns `true` when this span is longer than `other`. **Parameters** - `other` — the span to compare against **Returns**: `true` when this span is longer **Examples** ```kex 90.minutes.longerThan?(1.hours) # => true ``` ### `wholeMilliseconds` ```kex wholeMilliseconds : Integer ``` The whole milliseconds in the span, truncated toward zero. **Returns**: the milliseconds **Examples** ```kex 500.milliseconds.wholeMilliseconds # => 500 ``` ### `wholeSeconds` ```kex wholeSeconds : Integer ``` The whole seconds in the span, truncated toward zero. **Returns**: the seconds **Examples** ```kex Duration.hours(2).wholeSeconds # => 7200 ``` ### `wholeMinutes` ```kex wholeMinutes : Integer ``` The whole minutes in the span, truncated toward zero. **Returns**: the minutes **Examples** ```kex 90.minutes.wholeMinutes # => 90 1.5.hours.wholeMinutes # => 90 ``` ### `wholeHours` ```kex wholeHours : Integer ``` The whole hours in the span, truncated toward zero. A partial hour does not count. **Returns**: the hours **Examples** ```kex 90.minutes.wholeHours # => 1 ``` ### `wholeDays` ```kex wholeDays : Integer ``` The whole days in the span, truncated toward zero. This is what +date + span+ uses, which is why +date + 36.hours+ advances exactly one day. **Returns**: the days **Examples** ```kex 36.hours.wholeDays # => 1 2.weeks.wholeDays # => 14 ``` ### `wholeWeeks` ```kex wholeWeeks : Integer ``` The whole weeks in the span, truncated toward zero. **Returns**: the weeks **Examples** ```kex 14.days.wholeWeeks # => 2 ``` ### `compareTo` ```kex compareTo(other: Duration) -> Ordering ``` Orders this span against another by length. Delegates to `Number.compare` (algebra.kex), which orders the two Float second counts. **Parameters** - `other` — the span to compare against **Returns**: `Less`, `Equal` or `Greater` **Examples** ```kex 90.minutes.compareTo(60.minutes) # => Greater ``` _Sorting by length_ ```kex spans.sort { |a, b| a.compareTo(b) == Less } ``` ## module `Duration` Building elapsed spans, and UTC offsets. ### `zero` ```kex zero : Duration ``` A span of no time at all. Also the UTC offset. **Returns**: the zero span **Examples** ```kex Duration.zero().zero? # => true date.at(time, Duration.zero()).iso # => "...T09:00:00Z" ``` ### `nanoseconds` ```kex nanoseconds(count: Number) -> Duration ``` ### `microseconds` ```kex microseconds(count: Number) -> Duration ``` ### `milliseconds` ```kex milliseconds(count: Number) -> Duration ``` A span of `count` milliseconds. **Parameters** - `count` — how many milliseconds **Returns**: the elapsed span **Examples** ```kex Duration.milliseconds(1500).wholeSeconds # => 1 ``` ### `seconds` ```kex seconds(count: Number) -> Duration ``` A span of `count` seconds. **Parameters** - `count` — how many seconds **Returns**: the elapsed span **Examples** ```kex Duration.seconds(90).wholeMinutes # => 1 ``` ### `minutes` ```kex minutes(count: Number) -> Duration ``` A span of `count` minutes. **Parameters** - `count` — how many minutes **Returns**: the elapsed span **Examples** ```kex Duration.minutes(90).wholeHours # => 1 ``` ### `hours` ```kex hours(count: Number) -> Duration ``` A span of `count` hours. **Parameters** - `count` — how many hours **Returns**: the elapsed span **Examples** ```kex Duration.hours(2).wholeSeconds # => 7200 ``` ### `days` ```kex days(count: Number) -> Duration ``` A span of `count` days, each a fixed 86400 seconds. **Parameters** - `count` — how many days **Returns**: the elapsed span **Examples** ```kex Duration.days(2).wholeHours # => 48 ``` ### `weeks` ```kex weeks(count: Number) -> Duration ``` A span of `count` weeks, each a fixed 604800 seconds. **Parameters** - `count` — how many weeks **Returns**: the elapsed span **Examples** ```kex Duration.weeks(2).wholeDays # => 14 ``` ### `utcOffset` ```kex utcOffset(hours: Integer, minutes: Integer) -> Duration ``` A whole-minute UTC offset, the only kind ISO 8601 can spell. A negative hour or minute puts the whole offset west of UTC, so `utcOffset(-5, 30)` is five and a half hours behind UTC, not four and a half. **Parameters** - `hours` — the hour part of the offset - `minutes` — the minute part of the offset **Returns**: the offset **Examples** ```kex Duration.utcOffset(2, 0).wholeSeconds # => 7200 Duration.utcOffset(-5, 0).wholeSeconds # => -18000 ``` _Rendering an instant at another offset_ ```kex moment.at(Duration.utcOffset(-5, 0)).iso ``` ## module `Period` Building calendar spans. ### `zero` ```kex zero : Period ``` A span of nothing. **Returns**: the zero span **Examples** ```kex Period.zero().iso # => "P0D" Period.zero().zero? # => true ``` ### `of` ```kex of(years: Integer, months: Integer, days: Integer) -> Period ``` A span of the given years, months and days together. **Parameters** - `years` — whole years - `months` — whole months - `days` — whole days **Returns**: the calendar span **Examples** ```kex Period.of(1, 2, 3).iso # => "P1Y2M3D" ``` ### `years` ```kex years(count: Integer) -> Period ``` A span of `count` calendar years. **Parameters** - `count` — how many years **Returns**: the calendar span **Examples** ```kex (Date.of(2024, 2, 29).try + Period.years(1)).iso # => "2025-02-28" ``` ### `months` ```kex months(count: Integer) -> Period ``` A span of `count` calendar months. **Parameters** - `count` — how many months **Returns**: the calendar span **Examples** ```kex (Date.of(2026, 1, 31).try + Period.months(1)).iso # => "2026-02-28" ``` ### `days` ```kex days(count: Integer) -> Period ``` A span of `count` days, as a calendar step. **Parameters** - `count` — how many days **Returns**: the calendar span **Examples** ```kex Period.days(10).iso # => "P10D" ``` ### `weeks` ```kex weeks(count: Integer) -> Period ``` A span of `count` weeks, recorded as that many times seven days. **Parameters** - `count` — how many weeks **Returns**: the calendar span **Examples** ```kex Period.weeks(2).iso # => "P14D" ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: truthyable.kex title: Truthyable entities: - { kind: trait, name: "Truthyable" } - { kind: make, name: "Bool" } - { kind: make, name: "Integer" } - { kind: make, name: "Float" } - { kind: make, name: "String" } - { kind: make, name: "Optional" } - { kind: make, name: "[X]" } - { kind: make, name: "Map" } --- # Truthyable ## trait `Truthyable` `Truthyable`: what counts as true when a value is used as a condition. The rule is Crystal's, and it is short: only `false`, `None` and `()` are falsy. Everything else is truthy, including `0`, `""` and `[]`, which some languages treat as false and Kex deliberately does not. ```kex 0.truthy? # => true "".truthy? # => true [].truthy? # => true None.truthy? # => false false.truthy? # => false ``` When you want the "is there anything here" question instead, that is `Blankable`'s `blank?` / `present?`. There is no NaN to consider: a float operation that would produce one raises instead, matching BEAM (see nonFiniteFloatError in src/interpreter/value.cxx). Implemented by [`Bool`](#make-bool), [`Integer`](#make-integer), [`Float`](#make-float), [`String`](#make-string), [`Optional`](#make-optional), [`[X]`](#make-list), [`Map`](#make-map). ### Required functions #### `truthy?` ```kex truthy? : Bool ``` Returns `true` when the value counts as true in a condition. **Returns**: `true` unless the value is `false`, `None` or `()` **Examples** ```kex 1.truthy? # => true None.truthy? # => false ``` ## type `Bool` Implements [`Blankable`](blankable.md#trait-blankable), [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Returns the boolean itself. **Returns**: the value **Examples** ```kex true.truthy? # => true false.truthy? # => false ``` ### `not` ```kex not : Bool ``` Returns the negation of this boolean. `!flag` says the same thing, and is the spelling to reach for when the value is already to hand. This one exists for the position `!` cannot take: the end of a chain, where what is being negated is whatever the chain just produced. `falsy?` answers the same question for any `Truthyable` value; `not` is the one that both takes and answers a `Bool`. **Returns**: `false` for `true`, and `true` for `false` **Examples** ```kex true.not # => false false.not # => true ``` _At the end of a chain_ ```kex book.borrowed?.not ``` ### Defined in other modules - [Blankable](blankable.md#make-bool): [`blank?`](blankable.md#bool-blank?) ## extends `Integer` More functions for [`Integer`](number.md#make-integer), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Always `true`, including for zero. Zero is a number, not an absence. Compare it explicitly when zero means something: `count == 0`. **Returns**: always `true` **Examples** ```kex 0.truthy? # => true 42.truthy? # => true ``` ## extends `Float` More functions for [`Float`](number.md#make-float), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Always `true`, including for zero. **Returns**: always `true` **Examples** ```kex 0.0.truthy? # => true ``` ## extends `String` More functions for [`String`](string.md#make-string), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Always `true`, including for the empty string. Use `blank?` from `Blankable` when an empty or whitespace-only string should count as nothing. **Returns**: always `true` **Examples** ```kex "".truthy? # => true "".blank? # => true (the question usually meant) ``` ## extends `Optional` More functions for [`Optional`](optional.md#type-optional), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Returns `true` for a `Just` and `false` for `None`. The one type where truthiness is genuinely about presence, which is what makes an optional usable directly as a condition. **Returns**: `true` when a value is present **Examples** ```kex Just(0).truthy? # => true None.truthy? # => false ``` ## extends `[X]` More functions for [`List`](list.md#type-list), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Always `true`, including for the empty list. Use `empty?` or `blank?` when an empty list should count as nothing. **Returns**: always `true` **Examples** ```kex [].truthy? # => true [].blank? # => true (the question usually meant) ``` ## extends `Map` More functions for [`Map`](map.md#type-map), added by this module. Implements [`Truthyable`](#trait-truthyable). ### `truthy?` (from Truthyable) ```kex truthy? : Bool ``` Always `true`, including for the empty map. **Returns**: always `true` **Examples** ```kex {}.truthy? # => true {}.blank? # => true (the question usually meant) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: type.kex title: Type entities: - { kind: record, name: "Type" } - { kind: module, name: "Type" } - { kind: make, name: "Type.Field" } - { kind: make, name: "Type" } --- # Type ## record `Type` Types as values. `Type.of(x)` answers what a value IS, as something you can hold, print, compare, and take apart: ```kex Type.of(42) # Type { name: "Integer", args: [] } Type.of([1, 2]).to(String) # "[Integer]" Type.of(x) == Type.of(y) # structural equality, like any record Type.of(due).fieldNames # ["year", "month", "day"] ``` The answer comes from the compiler where it can: a checked expression knows things a value cannot carry, such as the unused half of a `Result` or the element type of an empty list. Where the checker has no concrete answer: gradual code, `--no-check`, a value arriving from another process: the value itself is asked instead. That fallback is honest but lossy: an empty list has no element to inspect, and a `Result` only ever holds one side. **Fields** - `name` : [String](string.md#make-string) - `args` : [[Type](#record-type)] - `pure` : [Bool](truthyable.md#make-bool) ### Functions Reflection functions on runtime type values. #### `fields` ```kex fields : [Type.Field] ``` Returns a record type's fields, in declaration order: each one's name, declared type, and whether a value may leave it out. Anything that is not a record answers with an empty list. The types are the DECLARED ones, as written: `Integer?` is `Type { name: "Option", args: [Integer] }` and a type parameter keeps its own name. That is what a decoder derived from a record needs: which parser each field takes, and which fields it may skip. **Returns**: the fields, or `[]` **Examples** ```kex Type.of(Point { x: 1, y: 2 }).fields.map(&.name) # => ["x", "y"] Type.of(Point { x: 1, y: 2 }).fields.first.map { |f| f.valueType.name } # => Just("Integer") Type.of(42).fields # => [] ``` _The fields a decoder may leave out_ ```kex Type.of(user).fields.filter { |f| f.optional? || f.default? }.map(&.name) ``` #### `fieldNames` ```kex fieldNames : [String] ``` Returns a record type's field names, in declaration order: `fields` when only the names matter. Anything that is not a record answers with an empty list. **Returns**: the field names, or `[]` **Examples** ```kex Type.of(Point { x: 1, y: 2 }).fieldNames # => ["x", "y"] Type.of(due).fieldNames # => ["year", "month", "day"] ``` _Rendering a record generically_ ```kex Type.of(value).fieldNames.map { |name| "${name}: ..." }.join(", ") ``` #### `constructors` ```kex constructors : [Type.Constructor] ``` Returns a sum type's constructors, in declaration order: each one's name and the declared types of its arguments. Anything that is not a sum type answers with an empty list. **Returns**: the constructors, or `[]` **Examples** ```kex Type.of(Circle(1.0)).constructors.map(&.name) # => ["Circle", "Square"] Type.of(Circle(1.0)).constructors.map(&.arity) # => [1, 2] Type.of(42).constructors # => [] ``` #### `constructorNames` ```kex constructorNames : [String] ``` Returns a sum type's constructor names, in declaration order: `constructors` when only the names matter. Anything that is not a sum type answers with an empty list. **Returns**: the constructor names, or `[]` **Examples** ```kex Type.of(Circle(1.0)).constructorNames # => ["Circle", "Square"] ``` _Listing what a sum type can be_ ```kex IO.printLine("one of: ${Type.of(shape).constructorNames.join(", ")}") ``` #### `constructorValues` ```kex constructorValues : [String] ``` The text of each nullary constructor in a database cell or form value, in declaration order. `ReadOnly` is `read_only`. #### `constructorValue` ```kex constructorValue(value: Any) -> String? ``` The stored value of a nullary constructor of this type. #### `construct` ```kex construct(values: {String: Any}) -> Result ``` Builds a record of this type from values by field name: the other half of `fields`, for a library mapping data it does not define (database rows, decoded JSON, form fields) onto the caller's records. A field missing from `values` takes its declared default, or `None` when it is optional. A required field that is missing, a key that names no field, and a value of the wrong type are each an error naming the field. Values are not converted: `"5"` is not an `Integer`, so convert first (`"5".to(Integer)`), using `fields` for the declared types. **Parameters** - `values` — the field values, by field name **Returns**: the record, or what was wrong **Examples** ```kex Type.named("User").construct({"id": "u1", "email": "ada@example.com"}) # => Ok(User { id: u1, email: ada@example.com, admin?: false }) Type.named("User").construct({"id": "u1", "mail": "x"}) # => Error("Unknown field \"mail\" for User") ``` #### `variant` ```kex variant(name: String, args: [Any]) -> Result ``` Builds the constructor `name` of this sum type from its arguments: reading `"Direct"` from a database back into `Direct`. The arguments are checked against the constructor's declared types, and their count against its arity. **Parameters** - `name` — the constructor's name, as `constructorNames` lists it - `args` — its arguments; leave them out for a nullary constructor **Returns**: the value, or what was wrong **Examples** ```kex Type.named("Kind").variant("Direct") # => Ok(Direct) Type.named("Shape").variant("Circle", [2.0]) # => Ok(Circle(2.0)) Type.named("Kind").variant("Nope") # => Error("Kind has no constructor Nope") ``` #### `parse` ```kex parse(text: String) -> Result ``` Parses `text` as a value of this type: what a database cell, a form field or a query parameter holds is text, and `construct` takes typed values. `String`, `Integer`, `Float`, `Bool` (`"true"` or `"false"`) and `Char` (one character) are read as written. `T?` parses a `T` and wraps it in `Just`. A sum type's nullary constructor accepts its exact name, case-insensitive name, or snake-case stored spelling: `"channel"` is `Channel`, and `"read_only"` is `ReadOnly`. Ambiguous normalized names are rejected. There is no null: text always stands for a value, and a caller with an absent one leaves it out of `construct`'s map. **Parameters** - `text` — the text to read **Returns**: the value, or why `text` is not one **Examples** ```kex Type.named("Integer").parse("42") # => Ok(42) Type.named("Integer").parse("4x") # => Error("Expected Integer, got \"4x\"") Type.named("Kind").parse("channel") # => Ok(Channel) ``` #### `nullaryNamed` ```kex nullaryNamed(text: String) -> String? ``` The nullary constructor `text` names, including its snake-case value. Exact constructor names take precedence; a normalized spelling must be unambiguous. #### `record?` ```kex record? : Bool ``` Returns `true` when the type is a record: that is, when it has fields. **Returns**: `true` for a record type **Examples** ```kex Type.of(Point { x: 1, y: 2 }).record? # => true Type.of(42).record? # => false ``` #### `adt?` ```kex adt? : Bool ``` Returns `true` when the type is a sum type: that is, when it has constructors. **Returns**: `true` for an ADT **Examples** ```kex Type.of(Circle(1)).adt? # => true Type.of(42).adt? # => false ``` #### `to` ```kex to(_) -> String? ``` Renders the type the way it is written in SOURCE, not the way it is stored. A list reads as `[Integer]`, a tuple as `(Integer, String)`, an optional as `String?`, a map as `{String: Integer}`, and a function as its signature. **Returns**: the type, as source **Examples** ```kex Type.of([1, 2]).to(String) # => Just("[Integer]") Type.of((1, "a")).to(String) # => Just("(Integer, String)") Type.of(Just(1)).to(String) # => Just("Integer?") Type.of("hi").to(String) # => Just("String") ``` _An error message that names the type_ ```kex die("cannot serialise a ${Type.of(value)}") ``` #### `showValue` ```kex showValue : String ``` The type as source, as `to(String)` gives it; also what interpolation shows. **Returns**: the type, as source **Examples** ```kex "${Type.of([1, 2])}" # => "[Integer]" ``` ## module `Type` Building and obtaining `Type` values. ### `of` ```kex of(value: Any) -> Type ``` Returns the type of `value`. The entry point to everything else here. Prefer it over matching on the `Type` record directly. A record or sum type's own name, passed as a value, stands for that type: `Type.of(User)` is `User`'s type. That is how a library takes a type from its caller, typed as the generic parameter it binds. **Parameters** - `value` — the value to inspect **Returns**: its type **Examples** ```kex Type.of(42) # => Type { name: "Integer", args: [] } Type.of([1, 2]).to(String) # => "[Integer]" Type.of("hi").to(String) # => "String" ``` _Reporting an unexpected value_ ```kex IO.printError("expected a list, got ${Type.of(value).to(String)}") ``` _Taking a type from the caller_ ```kex let select(as: T) -> Result<[T], String> do let model = Type.of(as) # the caller's record type ... # rows decoded with model.construct end select(as: User) # => Result<[User], String> ``` ### `named` ```kex named(name: String) -> Type ``` Builds a type from its name, for comparing against something you already know. **Parameters** - `name` — the type name **Returns**: the named type **Examples** ```kex Type.of(42) == Type.named("Integer") # => true ``` _Checking that a value came back as expected_ ```kex Type.of(parsed) == Type.named("Date") ``` ### `generic` ```kex generic(name: String, args: [Type]) -> Type ``` Builds a type that takes arguments, from its name and those arguments. **Parameters** - `name` — the type name - `args` — its type arguments **Returns**: the generic type **Examples** ```kex Type.of([1]) == Type.generic("List", [Type.named("Integer")]) # => true ``` ### `function` ```kex function(params: [Type], result: Type) -> Type ``` Builds a function type from its parameter types and its result type: in the order a signature is written. **Parameters** - `params` — the parameter types - `result` — the result type **Returns**: the function type **Examples** ```kex Type.function([Type.named("String")], Type.named("String")).to(String) # => "String -> String" ``` ### `returnedBy` ```kex returnedBy(function: Any) -> Type ``` Returns the type a named function returns, as answered by the compiler. Named functions only. A lambda or a function VALUE carries no signature at runtime, and an overloaded name has no single answer: both are compile errors rather than a guess. **Parameters** - `function` — a named function **Returns**: its return type **Examples** ```kex Type.returnedBy(Date.parse).to(String) # => "Result" ``` ### `fieldValues` ```kex fieldValues(value: Any) -> [Any]? ``` Returns a record's field values, in declaration order: the same order as `Type.of(record).fields`, so the two zip into name/value pairs. Anything that is not a record answers `None`; a tuple's elements are `items`. What a generic encoder needs: it reads a record's values without knowing its type, the way `construct` builds one. **Parameters** - `value` — the record to read **Returns**: its field values, or `None` **Examples** ```kex Type.fieldValues(Point { x: 1, y: 2 }) # => Just([1, 2]) Type.fieldValues((1, 2)) # => None ``` _A record as a map from field names_ ```kex Type.of(user).fields.map(&.name).zip(Type.fieldValues(user).or([])) ``` ### `parsed` ```kex parsed : Any? -> Type -> String -> Result ``` The result of a conversion that answers `None` for text it cannot read. ## record `Field` A record field, as `Type.fields` describes it. **Fields** - `name` : [String](string.md#make-string) - `valueType` : [Type](#record-type) - `optional?` : [Bool](truthyable.md#make-bool) - `default?` : [Bool](truthyable.md#make-bool) ## record `Constructor` A sum type's constructor, as `Type.constructors` describes it. **Fields** - `name` : [String](string.md#make-string) - `arity` : [Integer](number.md#make-integer) - `argTypes` : [[Type](#record-type)] ## type `Type.Field` ### `parse` ```kex parse(text: String) -> Result ``` Parses `text` as a value of this field's declared type: `Type.parse`, with the field named in the error. What a library reads from a database row or a form is text, and `Type.construct` takes typed values, so this is the step between them. **Parameters** - `text` — the text to read **Returns**: the value, or why `text` is not one **Examples** ```kex Type.named("User").fields.first.map { |field| field.parse("u1") } # => Just(Ok("u1")) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: units.kex title: Units entities: - { kind: trait, name: "Unit" } - { kind: function, name: "measureKind" } - { kind: function, name: "measureSymbol" } - { kind: function, name: "measureFactor" } - { kind: record, name: "UnitDefinition" } - { kind: make, name: "UnitDefinition" } - { kind: record, name: "Measure" } - { kind: record, name: "Duration" } - { kind: type, name: "TimeUnit" } - { kind: make, name: "TimeUnit" } - { kind: make, name: "Integer" } - { kind: make, name: "Float" } - { kind: make, name: "Measure" } - { kind: module, name: "Units" } --- # Units Numbers that carry a unit. Writing `5.second` or `90.minute` gives you a `Measure`: a number, the unit it was written in, and the same quantity in that dimension's base unit. Adding, subtracting and converting all go through the canonical value, so the arithmetic is right regardless of which units the operands were written in, and mixing dimensions is an `Error` rather than a silently wrong number. ```kex 5.second.to(String) # => Just("5.0 s") 1.5.hour.to(String) # => Just("1.5 h") (1.hour + 30.minute).map(~to(String)) # => Ok("1.5 h") 90.minute.convert(Hour) # => Ok(1.5 h) ``` The time units (`nanosecond` through `week`) are in the prelude. Other dimensions live in opt-in modules under `Units`, and every one of them measures against the same machinery here. A `Measure` is a measurement, not an elapsed span: `5.second` describes a quantity, while `Duration` is what `Time` and `Date` use for a span between two moments. ## trait `Unit` The trait a unit implements: how it converts to its dimension's base unit, which dimension that is, and how it is written. Implemented by [`UnitDefinition`](#make-unitdefinition), [`TimeUnit`](#make-timeunit), [`DataUnit`](units/data.md#make-dataunit), [`UnitDefinition`](units/data.md#make-unitdefinition), [`SIPrefix`](units/si.md#make-siprefix), [`SIUnit`](units/si.md#make-siunit), [`UnitDefinition`](units/si.md#make-unitdefinition). ### Required functions #### `factor` ```kex factor : Float ``` How many base units one of this unit is: `60.0` for a minute, whose base unit is the second. **Returns**: the conversion factor **Examples** ```kex Minute.factor # => 60.0 ``` #### `kind` ```kex kind : Atom ``` Which dimension this unit measures, as an atom. Only measures of the same kind can be added or converted between. **Returns**: the dimension tag **Examples** ```kex Second.kind # => :time ``` #### `symbol` ```kex symbol : String ``` How this unit is written when a measure is displayed. **Returns**: the unit's symbol **Examples** ```kex Minute.symbol # => "min" ``` ## function `measureKind` ```kex measureKind(measure: Measure) -> Atom ``` The dimension a measure belongs to. **Parameters** - `measure` — the measure to inspect **Returns**: the dimension tag **Examples** ```kex measureKind(5.second) # => :time ``` ## function `measureSymbol` ```kex measureSymbol(measure: Measure) -> String ``` The symbol a measure displays with. **Parameters** - `measure` — the measure to inspect **Returns**: the unit's symbol **Examples** ```kex measureSymbol(5.minute) # => "min" ``` ## function `measureFactor` ```kex measureFactor(measure: Measure) -> Float ``` The conversion factor of a measure's unit. **Parameters** - `measure` — the measure to inspect **Returns**: the conversion factor **Examples** ```kex measureFactor(5.minute) # => 60.0 ``` ## record `UnitDefinition` A unit described at run time rather than by a constructor. Runtime-defined units are used for prefixes and units derived by arithmetic: `s^2` from squaring a duration, a kilo- prefix applied to a base unit. **Fields** - `conversionFactor` : [Float](number.md#make-float) - `dimension` : [Atom](atom.md#make-atom) - `notation` : [String](string.md#make-string) Implements [`Unit`](#trait-unit). ### Functions #### `factor` (from Unit) How many base units one of this unit is. **Returns**: `Float` — the conversion factor #### `kind` (from Unit) Which dimension this unit measures. **Returns**: `Atom` — the dimension tag #### `symbol` (from Unit) How the unit is written. **Returns**: `String` — the unit's symbol ### Defined in other modules - [Units.Data](units/data.md#make-unitdefinition): [`factor`](units/data.md#unitdefinition-factor), [`kind`](units/data.md#unitdefinition-kind), [`symbol`](units/data.md#unitdefinition-symbol) - [Units.SI](units/si.md#make-unitdefinition): [`factor`](units/si.md#unitdefinition-factor), [`kind`](units/si.md#unitdefinition-kind), [`symbol`](units/si.md#unitdefinition-symbol) ## record `Measure` A quantity with a unit. A Measure is shared by every unit module. `canonical` stores the value in that dimension's base unit; its Unit controls its display. That split is what makes +1.hour + 30.minute+ correct and still print in hours. **Fields** - `canonical` : [Float](number.md#make-float) - `value` : [Float](number.md#make-float) - `unit` : [UnitDefinition](#record-unitdefinition) Implements [`Showable`](kex.md#trait-showable). ### Functions #### `factor` ```kex factor : Float ``` #### `kind` ```kex kind : Atom ``` #### `symbol` ```kex symbol : String ``` #### `to` ```kex to(String: Type, in: TimeUnit, format: NumberFormat) -> String? ``` Renders the measure in its own unit. **Parameters** - `String` — the target type **Returns**: the rendered measure **Examples** ```kex 90.second.to(String) # => Just("90.0 s") ``` #### `scaled` ```kex scaled : Measure ``` Chooses the largest unit whose absolute value is at least one. Time uses nanoseconds through weeks; data uses bytes through terabytes (`binary: true` selects KiB through TiB). SI dimensions use decimal prefixes. Zero uses the family's base unit; smaller values use its smallest unit. Other dimensions keep their current unit. **Examples** ```kex 90.second.scaled.to(String, Number.format`#.#`) # => Just("1.5 min") ``` ```kex scaled(binary: Bool) -> Measure ``` As `scaled`, using binary scales for data when `binary` is true. #### `showValue` (from Showable) ```kex showValue : String ``` Renders the measure as its value and unit, so `IO.printLine` and interpolation show `5.0 W` rather than the record's fields. Inspecting it (the REPL's echo, `inspected`) still shows the record itself. **Returns**: the value and the unit's symbol **Examples** ```kex "drawn: ${1.volt * 5.ampere}" # => "drawn: 5.0 W" ``` #### `scale` ```kex scale(multiplier: Number) -> Measure ``` Multiplies the measure by a plain number, keeping its unit. The unit is unchanged, so this scales a quantity rather than converting it: three of a two-second interval is six seconds. **Parameters** - `multiplier` — the factor to multiply by **Returns**: the scaled measure **Examples** ```kex 2.second.scale(3).to(String) # => Just("6.0 s") 1.hour.scale(0.5).to(String) # => Just("0.5 h") ``` #### `^` ```kex ^(exponent: Number) -> Measure ``` Raises the measure to a power, recording the derived unit in its notation. Raising a measure preserves its display unit while applying the power to its canonical value, display value, and conversion factor. The current Unit trait represents a dimension as one Atom, so the dimension remains the base kind; the notation records the derived unit (for example `s^2`). **Parameters** - `exponent` — the power to raise to **Returns**: the raised measure **Examples** ```kex (2.second ^ 2).to(String) # => Just("4.0 s^2") ``` #### `convertTo` ```kex convertTo(unit: Unit) -> Result ``` Converts the measure to another unit of the same dimension. Answers `Error` when the dimensions differ: you cannot express seconds in bytes, and this says so rather than producing a wrong number. Units from opt-in modules such as `Units.SI` and `Units.Data` can be used here too, provided their dimension matches. **Parameters** - `unit` — the unit to convert to **Returns**: the converted measure, or why it could not be **Examples** ```kex 1.day.convertTo(Hour).map { |m| m.showValue } # => Ok("24.0 h") 90.minute.convertTo(Hour).map { |m| m.showValue } # => Ok("1.5 h") ``` #### `convert` ```kex convert(unit: TimeUnit) -> Result ``` Converts the measure to another time unit. The short spelling of `convertTo`, kept for prelude time measures. **Parameters** - `unit` — the time unit to convert to **Returns**: the converted measure, or why it could not be **Examples** ```kex 90.minute.convert(Hour).map { |m| m.showValue } # => Ok("1.5 h") ``` #### `+` ```kex +(other: Measure) -> Result ``` Adds two measures of the same dimension. The units need not match: the sum goes through the canonical values, and comes back displayed in the LEFT operand's unit. Adding measures of different dimensions is an `Error`. **Parameters** - `other` — the measure to add **Returns**: the sum, or why they could not be added **Examples** ```kex (1.hour + 30.minute).map { |m| m.showValue } # => Ok("1.5 h") (30.minute + 1.hour).map { |m| m.showValue } # => Ok("90.0 min") ``` #### `-` ```kex -(other: Measure) -> Result ``` Subtracts a measure of the same dimension. Like +++, the result is displayed in the left operand's unit, and mixing dimensions is an `Error`. **Parameters** - `other` — the measure to subtract **Returns**: the difference, or why they could not be subtracted **Examples** ```kex (1.hour - 30.minute).map { |m| m.showValue } # => Ok("0.5 h") ``` #### `scaledValue` ```kex scaledValue(canonical: Float, factor: Float) -> Number ``` Divides a canonical value by a unit factor. The conversion is exact in principle (0.000005 W is 5 μW), but neither number is exactly representable in binary, so the quotient can land an ulp or two off (5.000000000000001). Round back to 15 significant digits, comfortably inside a double's ~15.95, so converted measures read the way they were written. The annotation says Number because `Integer / Float` infers Integer; the result is always a Float. #### `scaledMeasureBase` ```kex scaledMeasureBase(unit: UnitDefinition) -> UnitDefinition ``` #### `scaledMeasureFamily` ```kex scaledMeasureFamily(base: UnitDefinition, binary: Bool) -> [UnitDefinition] ``` ### Defined in other modules - [Units.Data](units/data.md#make-measure): [`factor`](units/data.md#measure-factor), [`kind`](units/data.md#measure-kind), [`symbol`](units/data.md#measure-symbol), [`to`](units/data.md#measure-to), [`<`](units/data.md#measure-op-lt), [`>`](units/data.md#measure-op-gt), [`<=`](units/data.md#measure-op-lt-eq), [`>=`](units/data.md#measure-op-gt-eq) - [Units.SI](units/si.md#make-measure): [`factor`](units/si.md#measure-factor), [`kind`](units/si.md#measure-kind), [`symbol`](units/si.md#measure-symbol), [`to`](units/si.md#measure-to), [`rescaled`](units/si.md#measure-rescaled), [`prefixScaled`](units/si.md#measure-prefixscaled), [`*`](units/si.md#measure-op-times), [`/`](units/si.md#measure-op-div), [`product`](units/si.md#measure-product), [`quotient`](units/si.md#measure-quotient), [`productKind`](units/si.md#measure-productkind), [`quotientKind`](units/si.md#measure-quotientkind), [`productSymbol`](units/si.md#measure-productsymbol), [`quotientSymbol`](units/si.md#measure-quotientsymbol) ## record `Duration` An elapsed span of time, in seconds. Duration is an elapsed span used by Time, Date, and DateTime. A time Measure is deliberately not a Duration: `5.second` describes a measurement. **Fields** - `seconds` : [Float](number.md#make-float) ### Defined in other modules - [Time](time.md#make-duration): [`wholeNanoseconds`](time.md#duration-wholenanoseconds), [`wholeMicroseconds`](time.md#duration-wholemicroseconds), [`+`](time.md#duration-op-plus), [`-`](time.md#duration-op-minus), [`negated`](time.md#duration-negated), [`*`](time.md#duration-op-times), [`/`](time.md#duration-op-div), [`abs`](time.md#duration-abs), [`zero?`](time.md#duration-zero?), [`negative?`](time.md#duration-negative?), [`positive?`](time.md#duration-positive?), [`shorterThan?`](time.md#duration-shorterthan?), [`longerThan?`](time.md#duration-longerthan?), [`wholeMilliseconds`](time.md#duration-wholemilliseconds), [`wholeSeconds`](time.md#duration-wholeseconds), [`wholeMinutes`](time.md#duration-wholeminutes), [`wholeHours`](time.md#duration-wholehours), [`wholeDays`](time.md#duration-wholedays), [`wholeWeeks`](time.md#duration-wholeweeks), [`compareTo`](time.md#duration-compareto) ## type `TimeUnit` The time units, from nanoseconds to weeks. The base unit is the second. **Variants** - `Nanosecond` - `Microsecond` - `Millisecond` - `Second` - `Minute` - `Hour` - `Day` - `Week` Implements [`Unit`](#trait-unit). ### Functions #### `factor` (from Unit) ```kex factor(_) ``` #### `kind` (from Unit) #### `symbol` (from Unit) ```kex symbol(_) ``` ## extends `Integer` More functions for [`Integer`](number.md#make-integer), added by this module. Time-unit constructors on `Integer`: `5.second`, `90.minute`, `2.week`. Each answers a `Measure` whose display unit is the one you named, so `90.minute` prints as minutes even though it is stored as 5400 seconds. ### `nanosecond` ```kex nanosecond : Measure ``` This many nanoseconds, as a `Measure`. **Returns**: the quantity, displayed in nanoseconds **Examples** ```kex 100.nanosecond.to(String) # => Just("100.0 ns") ``` ### `microsecond` ```kex microsecond : Measure ``` This many microseconds, as a `Measure`. **Returns**: the quantity, displayed in microseconds **Examples** ```kex 250.microsecond.to(String) # => Just("250.0 μs") ``` ### `millisecond` ```kex millisecond : Measure ``` This many milliseconds, as a `Measure`. **Returns**: the quantity, displayed in milliseconds **Examples** ```kex 500.millisecond.to(String) # => Just("500.0 ms") ``` ### `second` ```kex second : Measure ``` This many seconds, as a `Measure`. Seconds are the base unit of time, so this is the one everything else converts through. **Returns**: the quantity, displayed in seconds **Examples** ```kex 5.second.to(String) # => Just("5.0 s") ``` ### `minute` ```kex minute : Measure ``` This many minutes, as a `Measure`. **Returns**: the quantity, displayed in minutes **Examples** ```kex 90.minute.convert(Hour) # => Ok(1.5 h) ``` ### `hour` ```kex hour : Measure ``` This many hours, as a `Measure`. **Returns**: the quantity, displayed in hours **Examples** ```kex (1.hour + 30.minute).map { |m| m.showValue } # => Ok("1.5 h") ``` ### `day` ```kex day : Measure ``` This many days, as a `Measure`. **Returns**: the quantity, displayed in days **Examples** ```kex 1.day.convertTo(Hour).map { |m| m.showValue } # => Ok("24.0 h") ``` ### `week` ```kex week : Measure ``` This many weeks, as a `Measure`. **Returns**: the quantity, displayed in weeks **Examples** ```kex 2.week.to(String) # => Just("2.0 wk") ``` ### `timeMeasure` ```kex timeMeasure(unit: TimeUnit) -> Measure ``` ## extends `Float` More functions for [`Float`](number.md#make-float), added by this module. The same time-unit constructors on `Float`, for fractional quantities: `1.5.hour`, `0.25.second`. ### `nanosecond` ```kex nanosecond : Measure ``` This many nanoseconds, as a `Measure`. **Returns**: the quantity, displayed in nanoseconds ### `microsecond` ```kex microsecond : Measure ``` This many microseconds, as a `Measure`. **Returns**: the quantity, displayed in microseconds ### `millisecond` ```kex millisecond : Measure ``` This many milliseconds, as a `Measure`. **Returns**: the quantity, displayed in milliseconds **Examples** ```kex 2.5.millisecond.to(String) # => Just("2.5 ms") ``` ### `second` ```kex second : Measure ``` This many seconds, as a `Measure`. **Returns**: the quantity, displayed in seconds **Examples** ```kex 0.25.second.to(String) # => Just("0.25 s") ``` ### `minute` ```kex minute : Measure ``` This many minutes, as a `Measure`. **Returns**: the quantity, displayed in minutes ### `hour` ```kex hour : Measure ``` This many hours, as a `Measure`. **Returns**: the quantity, displayed in hours **Examples** ```kex 1.5.hour.to(String) # => Just("1.5 h") ``` ### `day` ```kex day : Measure ``` This many days, as a `Measure`. **Returns**: the quantity, displayed in days ### `week` ```kex week : Measure ``` This many weeks, as a `Measure`. **Returns**: the quantity, displayed in weeks ### `timeMeasure` ```kex timeMeasure(unit: TimeUnit) -> Measure ``` ## module `Units` Time-unit constructors, also available directly from the prelude. `Units.Hour` and `Hour` name the same constructor: they match the same patterns and compare equal. Use the qualified spelling to make the unit family explicit. ```kex 90.minute.convert(Units.Hour) # => Ok(1.5 h) ``` ### `Nanosecond` (constant) ### `Microsecond` (constant) ### `Millisecond` (constant) ### `Second` (constant) ### `Minute` (constant) ### `Hour` (constant) ### `Day` (constant) ### `Week` (constant) --- --- package: prelude version: "0.4.0-rc.3-dev" source: uri.kex title: URI entities: - { kind: module, name: "URI" } --- # URI ## module `URI` ### `parse` ```kex parse(text: String) -> Result ``` Strictly parses an ASCII RFC 3986 URI reference. Relative references are valid here. Use `URL.parse` when the input must be a complete hierarchical URL with a scheme and authority. **Parameters** - `text` — an absolute URI or relative reference **Returns**: the reference or a precise syntax error **Examples** _Resolving an image link found in a document_ ```kex let reference = URI.parse("../images/logo.svg").try let absolute = base.resolve(reference).try ``` ### `fromIRI` ```kex fromIRI(text: String) -> Result ``` Converts a Unicode IRI to an ASCII URI using IDNA and UTF-8 percent encoding. Use this for human-entered international addresses. `parse` is intentionally stricter and accepts only an already encoded ASCII URI. **Parameters** - `text` — a Unicode internationalized resource identifier **Returns**: the converted URI or a conversion error **Examples** _Turning a pasted international address into a request URI_ ```kex let target = URI.fromIRI("https://münich.example/straße").try HTTP.get(target.string) ``` ## record `URI` Strict RFC 3986 URI values, hierarchical URLs, query strings, and HTML form encoding. Parsing preserves caller spelling; normalization is always explicit. ```kex using URI let base = URL.parse("https://example.test/a/").try let reference = URI.parse("../items?limit=10").try base.resolve(reference).try.string ``` A parsed RFC 3986 URI reference. Construction is strict; use `parse` rather than building this representation directly. `string` preserves the spelling supplied by the caller, while `normalize` is explicit. **Fields** - `source` : [String](string.md#make-string) Implements [`Showable`](kex.md#trait-showable), [`Inspectable`](kex.md#trait-inspectable). ### Functions #### `string` ```kex string : String ``` Returns the caller-supplied URI spelling unchanged. **Returns**: the original URI text #### `normalize` ```kex normalize : URI ``` Returns an explicitly normalized RFC 3986 representation. Parsing never normalizes implicitly: signatures, cache keys, and logs may depend on the exact text received. Ask for normalization where semantic comparison is what you mean. **Returns**: the normalized URI #### `equivalent?` ```kex equivalent?(other: URI) -> Bool ``` Compares normalized representations rather than original spellings. **Parameters** - `other` — the URI to compare **Returns**: whether the URIs identify the same normalized reference **Examples** _Deduplicating differently spelled links_ ```kex URI.parse("HTTP://example.com/%7Eada").try .equivalent?(URI.parse("http://example.com/~ada").try) ``` #### `resolve` ```kex resolve(reference: URI) -> Result ``` Resolves a URI reference against this absolute base. **Parameters** - `reference` — the relative or absolute reference **Returns**: the resolved URI, or `NotAbsolute` when this receiver cannot serve as a base **Examples** _Following a relative link_ ```kex let base = URI.parse("https://example.com/docs/start").try base.resolve(URI.parse("../api").try).try.string # => "https://example.com/api" ``` #### `scheme` ```kex scheme : String? ``` Returns the normalized lowercase scheme, when one is present. **Returns**: the scheme, or `None` for a relative reference #### `host` ```kex host : Host? ``` Returns the parsed authority host, when one is present. **Returns**: the host, or `None` for references without an authority #### `query` ```kex query : Query? ``` Returns the parsed query when the reference contains `?`. `Just(Query { entries: [] })` represents an explicitly empty query; `None` means no question mark was present at all. **Returns**: the query, including an explicitly empty one #### `showValue` (from Showable) ```kex showValue : String ``` Renders with any authority password replaced by `***`. #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Structural inspection is also credential-safe. ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## record `URL` An absolute hierarchical URI with an authority component, such as an HTTP URL. Unlike a general `URI`, a `URL` always has a scheme and host, which makes accessors such as `scheme` and `host` total. **Fields** - `source` : [String](string.md#make-string) Implements [`Showable`](kex.md#trait-showable), [`Inspectable`](kex.md#trait-inspectable). ### Functions #### `string` ```kex string : String ``` Returns the caller-supplied URL spelling unchanged. **Returns**: the original URL text #### `normalize` ```kex normalize : URL ``` Returns an explicitly normalized URL without changing this value. **Returns**: the normalized URL #### `equivalent?` ```kex equivalent?(other: URL) -> Bool ``` Compares normalized representations rather than original spellings. **Parameters** - `other` — the URL to compare **Returns**: whether the URLs normalize to the same value #### `resolve` ```kex resolve(reference: URI) -> Result ``` Resolves a URI reference while preserving the URL invariant. **Parameters** - `reference` — the relative or absolute reference **Returns**: the resolved absolute URL **Examples** _Resolving an API pagination link_ ```kex let next = URL.parse("https://api.example.com/v1/items").try .resolve(URI.parse("?page=2").try) .try ``` #### `scheme` ```kex scheme : String ``` Returns the normalized lowercase scheme. **Returns**: the scheme #### `host` ```kex host : Host ``` Returns the authority host in display and normalized ASCII forms. **Returns**: the host #### `query` ```kex query : Query? ``` Returns the parsed query when the URL contains `?`. **Returns**: the query, or `None` when absent #### `showValue` (from Showable) ```kex showValue : String ``` Renders with any authority password replaced by `***`. #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Structural inspection is also credential-safe. ### From [`Showable`](kex.md#trait-showable) - [`to`](kex.md#showable-to) — ## record `Host` A host's display spelling and normalized ASCII/IDNA spelling. **Fields** - `display` : [String](string.md#make-string) - `ascii` : [String](string.md#make-string) ## record `Query` Ordered URI query entries. `None` distinguishes a bare key from `key=`. Order and duplicates matter in real APIs: `tag=kex&tag=beam` must not become a map with one value silently discarded. **Fields** - `entries` : [([String](string.md#make-string), [String](string.md#make-string)?)] ### Functions #### `encode` ```kex encode : String ``` Encodes according to generic RFC 3986 query rules. **Returns**: encoded query text without a leading question mark **Examples** ```kex Query.from([("q", Just("a+b"))]).encode # => "q=a%2Bb" ``` ## record `Form` Ordered `application/x-www-form-urlencoded` entries. This is deliberately separate from `Query`: HTML forms encode spaces as plus signs, while a generic URI query treats a plus as an ordinary +++. **Fields** - `entries` : [([String](string.md#make-string), [String](string.md#make-string)?)] ### Functions #### `encode` ```kex encode : String ``` Encodes according to HTML form rules, including space as ++. **Returns**: the `application/x-www-form-urlencoded` body **Examples** ```kex Form.from([("query", "hello world")]).encode # => "query=hello+world" ``` ## record `URIError` A typed URI parsing, conversion, or resolution failure. **Fields** - `kind` : [URIErrorKind](#type-uri-urierrorkind) - `message` : [String](string.md#make-string) - `position` : [Integer](number.md#make-integer)? ## type `URIErrorKind` Stable URI failure categories. **Variants** - `InvalidSyntax` - `InvalidEscape` - `InvalidAuthority` - `InvalidPort` - `NonASCII` - `NotAbsolute` - `NotHierarchical` - `MissingAuthority` - `UnsupportedScheme` ## module `URI.URL` ### `parse` ```kex parse(text: String) -> Result ``` Parses an absolute hierarchical URL with an authority. Rejects relative references, opaque URIs, and values without a host, so a caller can use `scheme` and `host` without handling absence. **Parameters** - `text` — the complete URL **Returns**: the URL or a precise syntax error **Examples** _Validating a webhook target at configuration time_ ```kex let webhook = URL.parse(ENV.get("WEBHOOK_URL").or("")).try ``` ### `build` ```kex build : String -> String -> [String] -> Query -> Result ``` Builds and validates a URL from decoded path segments and a query value. Pass decoded values, not pre-escaped text. The builder escapes each path segment independently, so a slash inside one value cannot accidentally become another level of the path. **Parameters** - `scheme` — the URL scheme, such as `https` - `host` — the Unicode or ASCII hostname - `path` — decoded path segments - `query` — ordered decoded query entries **Returns**: the encoded, validated URL **Examples** _Building an API URL from an identifier supplied by a user_ ```kex URL.build( "https", "api.example.com", ["users", userName], Query.from([("include", Just("profile"))]) ).try ``` ## module `URI.Query` ### `from` ```kex from(entries: [(String, String?)]) -> Query from(entries: Map) -> Query ``` Builds a query while preserving order, duplicates, and bare keys. A `None` value encodes as a bare key; `Just("")` encodes with an equals sign. This preserves the difference between `?debug` and `?debug=`. **Parameters** - `entries` — decoded key/value pairs **Returns**: the ordered query **Examples** _Adding repeated filters and a bare feature flag_ ```kex Query.from([ ("tag", Just("kex")), ("tag", Just("beam")), ("debug", None) ]) ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses generic URI query encoding; ++ remains a literal plus. **Parameters** - `text` — encoded query text without the leading question mark **Returns**: decoded entries or an invalid escape **Examples** _Reading repeated filters without losing their order_ ```kex let filters = Query.parse("tag=kex&tag=beam&debug").try.entries ``` ## module `URI.Form` ### `from` ```kex from(entries: [(String, String)]) -> Form from(entries: Map) -> Form ``` Builds a form value while preserving order and duplicates. **Parameters** - `entries` — decoded form fields **Returns**: the ordered form **Examples** _Preparing a login request body_ ```kex Form.from([("email", email), ("password", password)]).encode ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses form encoding where ++ represents a space. **Parameters** - `text` — an `application/x-www-form-urlencoded` body **Returns**: decoded fields or an invalid escape **Examples** _Reading a search field submitted by a browser_ ```kex Form.parse("query=hello+world").try.entries ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: control/retry.kex title: Control.Retry entities: - { kind: module, name: "Control.Retry" } --- # Control.Retry ## module `Control.Retry` Retry an operation after it returns an error. **Examples** _Retry a request and handle the final result_ ```kex using Control.Retry using Net.HTTP let result = Retry.run(attempts: 5) do HTTP.get("https://api.example.com/inventory") end match result do Ok(response) => IO.printLine(response.status.code) Error(error) => IO.printLine(error.message) end ``` _Fixed delays: three waits of 250 milliseconds_ ```kex let schedule = Retry.Schedule { attempts: 4, delay: 250.milliseconds, backoff: 1.0, maximumDelay: 250.milliseconds, jitter: 0.0 } Retry.run(schedule: schedule) do Error("not ready") end # => Error("not ready"), after four calls and 750 ms of sleep ``` _Exponential delays: 200, 400, 800, 1600 milliseconds_ ```kex let schedule = Retry.Schedule { attempts: 5, delay: 200.milliseconds, backoff: 2.0, maximumDelay: 5.seconds, jitter: 0.0 } Retry.run(schedule: schedule) do Error("busy") end # => Error("busy"), after five calls and three seconds of sleep ``` _Capped delays: 500 ms, 1 s, 2 s, 2 s, 2 s_ ```kex Retry.Schedule { attempts: 6, delay: 500.milliseconds, backoff: 2.0, maximumDelay: 2.seconds, jitter: 0.0 } ``` _Use one schedule for two requests_ ```kex using Net.HTTP let schedule = Retry.Schedule { attempts: 5 } let inventory = Retry.run(schedule: schedule) do HTTP.get("https://api.example.com/inventory") end let orders = Retry.run(schedule: schedule) do HTTP.get("https://api.example.com/orders") end # Each request gets up to five attempts, independently. ``` _Set retry options directly_ ```kex Retry.run(attempts: 5, delay: 200.milliseconds, jitter: 0.0) do Ok("ready") end # => Ok("ready") ``` _Limit the time spent sleeping_ ```kex Retry.run( attempts: 5, delay: 1.seconds, backoff: 2.0, jitter: 0.0, maximumTotalDelay: Just(2.seconds) ) do Error("busy") end # => Error("busy"), two calls and one second of sleep ``` _Test jitter without waiting_ ```kex Retry.run( attempts: 2, delay: 4.seconds, jitter: 0.25, sleeper: { |wait| Assert.equal(wait, 3.seconds) }, random: { 0.0 } ) do Error("temporary") end # => Error("temporary"), two calls and one fake sleep ``` _Report progress_ ```kex using Net.HTTP Retry.run(attempts: 5, onRetry: { |info| let progress = "retrying ${info.nextAttempt}/${info.maximumAttempts}" IO.printLine("${progress} in ${info.delay.seconds} seconds") }) do HTTP.get("https://api.example.com/inventory") end ``` _Retry temporary HTTP failures and selected statuses_ ```kex using Net using Net.HTTP let schedule = Retry.Schedule { attempts: 5 } let outcome = Retry.run(schedule: schedule) do let result = HTTP.get("https://api.example.com/inventory") match result do Error(error) => if error.kind == Timeout || error.kind == Connect Retry.again(result) else Retry.done(result) end Ok(response) => if [429, 502, 503, 504].contains?(response.status.code) Retry.again(result) else Retry.done(result) end end end match outcome do Done(result) => IO.printLine(result) Again(last) => IO.printLine("retry budget exhausted: ${last}") end ``` ## module `Control.Retry.Retry` `run` calls the block immediately. `Ok` stops; `Error` waits and retries. When the schedule runs out, it returns the last error unchanged. Use `again` and `done` when your application needs to choose what to retry. HTTP 429 and 503 responses are `Ok(response)`, so retrying those statuses requires an explicit decision. For writes, make sure repeating the request is safe, for example by using an idempotency key. Set request timeouts separately: the schedule limits sleep, not time spent inside the operation. ### `again` ```kex again(value: X) -> Decision ``` Retry this operation if the schedule has room for another attempt. **Parameters** - `value` — the last outcome, retained if the schedule is exhausted **Returns**: `Again(value)` ### `done` ```kex done(value: X) -> Decision ``` Stop now. The supplied value may be a success or a permanent failure. **Parameters** - `value` — the final application outcome **Returns**: `Done(value)` ### `run` ```kex run : Block -> Schedule -> Integer -> Duration -> Float -> Duration -> Float -> Duration? -> (Duration -> Void) -> Block -> (Info -> Void) -> X ``` Call the block until it succeeds, returns `done`, or runs out of attempts or sleep time. The first attempt is immediate. Every later attempt follows one sleep. There is no sleep after success or the final error. Named timing options override the corresponding schedule field for this execution only. Return a `Result` from the block for automatic retries: `Ok` stops and `Error` retries. If no more retries are allowed, `run` returns the last error. Return `done(value)` or `again(value)` to make the decision yourself. The final `Done` means the block chose to stop; `Again` means it wanted another try but the schedule ran out. Use one return form throughout the block. Runtime faults propagate; they are not retried. Out-of-range settings are adjusted: attempts below one become one, negative durations become zero, backoff below one becomes one, jitter and random samples are clamped to 0..1. Non-finite Float settings are unsupported. `Done(Error(...))` means the application stopped on a permanent error. `Again(Ok(response))` means an unacceptable HTTP status persisted until exhaustion. HTTP status classification and Retry-After handling are the application's responsibility; `run` has no HTTP-specific behavior. The testing hooks are independent: replacing sleep keeps normal random sampling, and replacing randomness keeps real sleep. A random sample of 0 selects the lower jitter bound, 0.5 the base, and 1 the upper bound. With zero jitter no random sample is needed. lint:allow parameter-count — named options make each setting independent **Parameters** - `operation` — called again for each attempt; returns Result or Decision - `schedule` — reusable settings; defaults to `Schedule {}` - `attempts` — total attempts; defaults to the schedule field - `delay` — initial wait; defaults to the schedule field - `backoff` — delay multiplier; defaults to the schedule field - `maximumDelay` — sleep cap; defaults to the schedule field - `jitter` — variation fraction; defaults to the schedule field - `maximumTotalDelay` — sleep budget; defaults to the field - `sleeper` — defaults to real `Task.sleep` - `random` — defaults to secure backend sampling in 0..1 - `onRetry` — reports an allowed retry; defaults to no action **Returns**: first Ok/Done, or last Error/Again when retries run out ## record `Schedule` Timing and attempt limits for `run`. Defaults: three attempts, starting with a 100 ms wait, doubling up to 5 seconds, with 20% jitter. All fields are optional. A schedule holds settings. Reusing it starts each run from attempt one. * `attempts`: maximum executions, including the first; default 3. * `delay`: initial base wait; default 100 milliseconds. * `backoff`: base-delay multiplier; default 2.0, or 1.0 for fixed waits. * `maximumDelay`: cap on each actual wait, including jitter; default 5 seconds. * `jitter`: symmetric proportional variation; default 0.2, or 0.0 to disable. * `maximumTotalDelay`: cumulative sleep allowance; default `None`. Operation execution time is excluded. A wait that exceeds the remaining allowance ends the run without sleeping or calling the operation again. **Fields** - `attempts` : [Integer](../number.md#make-integer) (optional) - `delay` : [Duration](../units.md#record-duration) (optional) - `backoff` : [Float](../number.md#make-float) (optional) - `maximumDelay` : [Duration](../units.md#record-duration) (optional) - `jitter` : [Float](../number.md#make-float) (optional) - `maximumTotalDelay` : [Duration](../units.md#record-duration)? (optional) ## type `Decision` Whether to retry, together with the last result. `Done(value)` means the block stopped deliberately. `Again(value)` returned by `run` means the schedule was exhausted before completion. The value inside can be any application result, including an `Error`. **Variants** - `Again(X)` - `Done(X)` ## record `Info` Progress passed to `onRetry` before the next wait. The callback runs only when another attempt is allowed. It does not run for the first call, a successful result, `done`, or exhaustion. Durations count scheduled sleep; they exclude time spent in the operation. * `attempt`: the just-completed attempt, starting at 1. * `nextAttempt`: the attempt that follows the upcoming wait. * `maximumAttempts`: total permitted executions, including the first. * `remainingAttempts`: executions remaining, including the upcoming one. * `delay`: actual upcoming wait, after jitter and the delay cap. * `totalDelay`: sleep already performed, excluding the upcoming wait. * `result`: the last `Error` or `Again`, including its application payload. **Fields** - `attempt` : [Integer](../number.md#make-integer) - `nextAttempt` : [Integer](../number.md#make-integer) - `maximumAttempts` : [Integer](../number.md#make-integer) - `remainingAttempts` : [Integer](../number.md#make-integer) - `delay` : [Duration](../units.md#record-duration) - `totalDelay` : [Duration](../units.md#record-duration) - `result` : X --- --- package: prelude version: "0.4.0-rc.3-dev" source: data/queue.kex title: Data.Queue entities: - { kind: module, name: "Data" } --- # Data.Queue ## module `Data` A first-in-first-out queue. Opt-in: nothing here is in scope until `using Data.Queue`. ```kex using Data.Queue ``` A banker's queue: `front` holds the elements ready to leave, `back` holds what was most recently added, reversed. `enqueue` conses onto `back`; `dequeue` takes `front`'s head, and only rotates `back` (reversing it into `front`) when `front` runs out. Both operations are amortized O(1), versus the O(n) `enqueue` a single-list queue would pay for. ```kex let q = Queue.from([1, 2, 3]) q.enqueue(4).items # => [1, 2, 3, 4] q.dequeue # => Just((1, Queue(2, 3))) q.peek # => Just(1) ``` `items` is +@front + @back.reverse+, so the queue is not opaque: it is always a real list you can hand to anything, the same promise `Data.Set` makes. Unlike a set, a queue's representation is NOT canonical: `Queue.from([1,2])` and `Queue.from([1]).enqueue(2)` hold the same elements in different `front`/`back` splits, so they are structurally unequal even though they answer the same to every function. `==` is therefore overloaded to compare `items` rather than the record fields directly, but that overload only reaches ordinary `==` calls. Two such queues used as map keys, or matched against each other as record patterns, still compare structurally on both backends, and can disagree with `==`. Updates return a new queue rather than changing the receiver. `enqueue!` and `dequeue!` come free from the `!` rebinding form. ## record `Queue
` A queue of elements, split into a ready-to-leave `front` and a most-recently-added, reversed `back`. Build one with `Queue.from` rather than by hand. **Fields** - `front` : [A] (optional) - `back` : [A] (optional) Implements [`Enumerable`](../enumerable.md#trait-enumerable), [`Foldable`](../enumerable.md#trait-foldable), [`Monoid`](../algebra.md#trait-monoid), [`Showable`](../kex.md#trait-showable), [`Blankable`](../blankable.md#trait-blankable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: B, f: (B -> A -> B)) -> B ``` Folds from front to back. This is `Queue`'s `Enumerable`/`Foldable` primitive. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each element **Returns**: the final accumulator **Examples** ```kex Queue.from([1, 2, 3]).reduce(0) { |sum, x| sum + x } # => 6 ``` #### `identity` (from Monoid) ```kex identity : Queue ``` The empty queue: the `Monoid` identity. **Returns**: the empty queue #### `combine` (from Monoid) ```kex combine(other: Queue) -> Queue ``` Combines two queues, this one's elements followed by the argument's. **Parameters** - `other` — the queue to append **Returns**: this queue's elements, then `other`'s **Examples** ```kex Queue.from([1, 2]).combine(Queue.from([3, 4])).items # => [1, 2, 3, 4] ``` #### `items` ```kex items : [A] ``` Returns the elements front to back. Never opaque: this is a real list. **Returns**: the elements, front first **Examples** ```kex Queue.from([1, 2]).enqueue(3).items # => [1, 2, 3] ``` #### `enqueue` ```kex enqueue(value: A) -> Queue ``` Returns a new queue with `value` added at the back. Use `enqueue!` to rebind the receiver variable. **Parameters** - `value` — the element to add **Returns**: a queue with `value` at the back **Examples** ```kex Queue.from([1, 2]).enqueue(3).items # => [1, 2, 3] ``` #### `dequeue` ```kex dequeue : (A, Queue)? ``` Returns the front element and the queue without it, wrapped in `Just`, or `None` for an empty queue. Rotates `back` into `front` (reversing it) when `front` has run out: the one case that is not O(1), and only amortized so because each element is reversed at most once over the queue's lifetime. Use `dequeue!` to rebind the receiver variable. **Returns**: the front element and the rest, or `None` **Examples** ```kex Queue.from([1, 2, 3]).dequeue # => Just((1, Queue(2, 3))) Queue.empty.dequeue # => None ``` #### `peek` ```kex peek : A? ``` Returns the front element wrapped in `Just`, or `None` for an empty queue. **Returns**: the front element, or `None` **Examples** ```kex Queue.from([1, 2]).peek # => Just(1) Queue.empty.peek # => None ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of elements. **Returns**: the number of elements **Examples** ```kex Queue.from([1, 2, 3]).count # => 3 ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the queue has no elements. **Returns**: `true` for the empty queue **Examples** ```kex Queue.empty.empty? # => true Queue.from([1]).empty? # => false ``` #### `==` ```kex ==(other: Queue) -> Bool ``` Compares two queues by their elements, front to back: NOT by their `front`/`back` split, which is not canonical. See the file header. **Parameters** - `other` — the queue to compare against **Returns**: `true` when both hold the same elements in the same order **Examples** ```kex Queue.from([1, 2]) == Queue.from([1]).enqueue(2) # => true ``` #### `+` ```kex +(other: Queue) -> Queue +(other: [A]) -> Queue ``` Appends another queue's elements, or a plain list's. **Parameters** - `other` — the elements to append **Returns**: this queue's elements, then `other`'s **Examples** ```kex Queue.from([1, 2]) + [3, 4] # => Queue(1, 2, 3, 4) Queue.from([1, 2]) + Queue.from([3]) # => Queue(1, 2, 3) ``` #### `showValue` (from Showable) ```kex showValue : String ``` Renders the queue as `Queue(...)`, front to back. **Returns**: the rendered queue **Examples** ```kex Queue.from([1, 2, 3]).showValue # => "Queue(1, 2, 3)" ``` #### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the queue has no elements. The `Blankable` view of `empty?`. **Returns**: `true` for the empty queue **Examples** ```kex Queue.empty.blank? # => true Queue.from([1]).blank? # => false ``` ### From [`Enumerable`](../enumerable.md#trait-enumerable) - [`map`](../enumerable.md#enumerable-map) — Applies `f` to each item and collects the results into a list. - [`mapIndexed`](../enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`filter`](../enumerable.md#enumerable-filter) — Returns the items for which `pred` answers `true`. - [`flatMap`](../enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](../enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](../enumerable.md#trait-foldable) - [`each`](../enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](../enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](../enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](../enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](../enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. ### From [`Monoid`](../algebra.md#trait-monoid) - [`repeat`](../algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Showable`](../kex.md#trait-showable) - [`to`](../kex.md#showable-to) — ## module `Data.Queue` Constructors for `Queue`. ### `from` ```kex from(items: [A]) -> Queue ``` Builds a queue from a list, front to back. **Parameters** - `items` — the elements, front first **Returns**: the queue, ready to `dequeue` in the same order **Examples** ```kex Queue.from([1, 2, 3]).peek # => Just(1) ``` ### `empty` (constant) ```kex empty : Queue ``` The queue with no elements. Also the `Monoid` identity. **Examples** ```kex Queue.empty.empty? # => true ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: data/set.kex title: Data.Set entities: - { kind: module, name: "Data" } --- # Data.Set ## module `Data` Immutable collections of distinct elements. Opt-in: nothing here is in scope until `using Data.Set`, which brings both flavours below into scope at once. ```kex using Data.Set ``` Membership is decided by structural equality: the same equality `==` and map keys use, so records and tuples are compared by value, not identity. Updates return a new set; the `!` forms (`add!`, `delete!`) build a new set and rebind the receiver variable rather than modifying anything in place. ```kex let tags = Set.from(["kex", "beam", "kex"]) tags.count # => 2 tags.contains?("beam") # => true tags.add("erlang").items # => ["beam", "erlang", "kex"] ``` There are two flavours, differing only in how they store their elements: ```kex Set sorted, so iteration is in ascending element order and the elements must be Orderable. UnorderedSet hash-backed, so membership does not pay for ordering and the elements need only be comparable. Iteration order is unspecified: never write a test against it. ``` Reach for `Set` when you will read the elements back out, and for `UnorderedSet` when the set exists to answer `contains?` quickly. Sets compare by their elements. Use `items` to obtain a list; `UnorderedSet.items` has no guaranteed order. ## record `Set` A set whose elements are kept sorted and duplicate free. Build one with `Set.from` rather than by hand: the record literal does no deduplication and no sorting, and every function here relies on both. Reading `items` back is the field itself, so handing a set's elements to list code costs nothing. ```kex Set.from([3, 1, 2]).items # => [1, 2, 3] ``` **Fields** - `items` : [A] (optional) Implements [`Enumerable`](../enumerable.md#trait-enumerable), [`Foldable`](../enumerable.md#trait-foldable), [`Monoid`](../algebra.md#trait-monoid), [`Showable`](../kex.md#trait-showable), [`Blankable`](../blankable.md#trait-blankable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: B, f: (B -> A -> B)) -> B ``` Folds over the elements in ascending order. This is `Set`'s `Enumerable` primitive; `each`, `find`, `any?` and the rest are built on it. The collection-returning operations are overridden below, because `Enumerable`'s defaults answer with a list. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each element **Returns**: the final accumulator **Examples** ```kex Set.from([1, 2, 3]).reduce(0) { |sum, x| sum + x } # => 6 ``` #### `identity` (from Monoid) ```kex identity : Set ``` The empty set: the `Monoid` identity, since union with nothing changes nothing. **Returns**: the empty set #### `combine` (from Monoid) ```kex combine(other: Set) -> Set ``` Combines two sets by union. The `Monoid` operation. **Parameters** - `other` — the set to combine with **Returns**: every element of either set **Examples** _Merging many sets into one_ ```kex [Set.from([1]), Set.from([2]), Set.from([1, 3])] .reduce(Set.empty) { |acc, s| acc.combine(s) } # => Set(1, 2, 3) ``` #### `contains?` ```kex contains?(value: A) -> Bool ``` Returns `true` when `value` is a member. **Parameters** - `value` — the element to look for **Returns**: `true` when it is present **Examples** ```kex Set.from([1, 2, 3]).contains?(2) # => true Set.from([1, 2, 3]).contains?(9) # => false ``` _Filtering a list against an allow-list_ ```kex let allowed = Set.from(["get", "post"]) methods.filter { |m| allowed.contains?(m.lowerCase) } ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of distinct elements. Duplicates in the source list were already discarded, so this is the size of the set, not of what it was built from. **Returns**: the number of elements **Examples** ```kex Set.from([1, 1, 2]).count # => 2 ``` _Counting distinct words_ ```kex Set.from(text.split(" ")).count ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the set has no elements. **Returns**: `true` for the empty set **Examples** ```kex Set.empty.empty? # => true Set.from([1]).empty? # => false ``` #### `add` ```kex add(value: A) -> Set ``` Returns a new set with `value` added. Adding an element that is already a member changes nothing: that is what makes a set a set. Use `add!` to rebind the receiver variable. **Parameters** - `value` — the element to add **Returns**: a set including `value` **Examples** ```kex Set.from([1, 2]).add(3) # => Set(1, 2, 3) Set.from([1, 2]).add(2) # => Set(1, 2) ``` _Accumulating as you go_ ```kex var seen = Set.empty ids.each { |id| seen.add!(id) } ``` #### `delete` ```kex delete(value: A) -> Set ``` Returns a new set without `value`. Removing something that is not a member changes nothing. Use `delete!` to rebind the receiver variable. **Parameters** - `value` — the element to remove **Returns**: a set without `value` **Examples** ```kex Set.from([1, 2, 3]).delete(2) # => Set(1, 3) Set.from([1, 2, 3]).delete(9) # => Set(1, 2, 3) ``` #### `union` ```kex union(other: Set) -> Set ``` Returns every element of either set. **Parameters** - `other` — the other set **Returns**: the union **Examples** ```kex Set.from([1, 2]).union(Set.from([2, 3])) # => Set(1, 2, 3) ``` _Collecting every tag used across posts_ ```kex posts.reduce(Set.empty) { |all, p| all.union(Set.from(p.tags)) } ``` #### `intersect` ```kex intersect(other: Set) -> Set ``` Returns the elements both sets have. **Parameters** - `other` — the other set **Returns**: the intersection **Examples** ```kex Set.from([1, 2, 3]).intersect(Set.from([2, 3, 4])) # => Set(2, 3) Set.from([1]).intersect(Set.from([2])) # => Set() ``` _Which requested permissions the user actually has_ ```kex requested.intersect(granted) ``` #### `difference` ```kex difference(other: Set) -> Set ``` Returns the elements of this set that `other` does not have. Order matters: `a.difference(b)` and `b.difference(a)` are different questions. Use `symmetricDifference` when you want both answers. **Parameters** - `other` — the set to subtract **Returns**: the elements only this set has **Examples** ```kex Set.from([1, 2, 3]).difference(Set.from([2])) # => Set(1, 3) ``` _Which required fields are still missing_ ```kex required.difference(Set.from(form.keys)) ``` #### `symmetricDifference` ```kex symmetricDifference(other: Set) -> Set ``` Returns the elements in exactly one of the two sets: everything they do not agree on. **Parameters** - `other` — the other set **Returns**: the symmetric difference **Examples** ```kex Set.from([1, 2]).symmetricDifference(Set.from([2, 3])) # => Set(1, 3) ``` _What changed between two snapshots_ ```kex before.symmetricDifference(after) ``` #### `subset?` ```kex subset?(other: Set) -> Bool ``` Returns `true` when every element of this set is also in `other`. The empty set is a subset of everything. **Parameters** - `other` — the candidate superset **Returns**: `true` when this set is contained in `other` **Examples** ```kex Set.from([1, 2]).subset?(Set.from([1, 2, 3])) # => true Set.from([1, 9]).subset?(Set.from([1, 2, 3])) # => false ``` _An authorisation check_ ```kex required.subset?(granted) ``` #### `superset?` ```kex superset?(other: Set) -> Bool ``` Returns `true` when this set has every element of `other`. The mirror image of `subset?`. **Parameters** - `other` — the candidate subset **Returns**: `true` when this set contains all of `other` **Examples** ```kex Set.from([1, 2, 3]).superset?(Set.from([1, 2])) # => true Set.from([1, 2]).superset?(Set.from([1, 9])) # => false ``` #### `disjoint?` ```kex disjoint?(other: Set) -> Bool ``` Returns `true` when the two sets share no element. **Parameters** - `other` — the other set **Returns**: `true` when the intersection is empty **Examples** ```kex Set.from([1, 2]).disjoint?(Set.from([3])) # => true Set.from([1, 2]).disjoint?(Set.from([2, 3])) # => false ``` _Checking that two rulesets cannot both apply_ ```kex allowList.disjoint?(denyList) ``` #### `+` ```kex +(other: Set) -> Set +(other: [A]) -> Set ``` Unions with another set, or with a plain list. The list form is the everyday way to add one element without naming a function: +s + [x]+. **Parameters** - `other` — the elements to add **Returns**: the union **Examples** ```kex Set.from([1, 2]) + [3] # => Set(1, 2, 3) Set.from([1]) + Set.from([2]) # => Set(1, 2) ``` #### `-` ```kex -(other: Set) -> Set -(other: [A]) -> Set ``` Removes another set's elements, or a plain list's. **Parameters** - `other` — the elements to remove **Returns**: the difference **Examples** ```kex Set.from([1, 2, 3]) - [2] # => Set(1, 3) Set.from([1, 2, 3]) - Set.from([2, 3]) # => Set(1) ``` #### `map` (from Enumerable) ```kex map(f: (A -> B)) -> Set ``` Applies `f` to every element and returns a set of the results. Mapping a set may collapse elements: if `f` sends two members to the same value, the result has one. That is not a loss of information so much as the point of a set: `Set.from([1, -1]).map(~abs)` has one member. **Parameters** - `f` — applied to each element **Returns**: the distinct results **Examples** ```kex Set.from([1, 2, 3]).map { |x| x * 2 } # => Set(2, 4, 6) Set.from([1, -1]).map { |x| x.abs } # => Set(1) ``` _Collecting the distinct extensions in a file list_ ```kex Set.from(paths).map { |p| p.split(".").last.or("") } ``` #### `filter` (from Enumerable) ```kex filter(pred: (A -> Bool)) -> Set ``` Returns a new set with only the elements `pred` accepts. **Parameters** - `pred` — the test applied to each element **Returns**: the matching elements **Examples** ```kex Set.from([1, 2, 3]).filter { |x| x > 1 } # => Set(2, 3) ``` #### `reject` ```kex reject(pred: (A -> Bool)) -> Set ``` Returns a new set without the elements `pred` accepts. The complement of `filter`. **Parameters** - `pred` — the test applied to each element **Returns**: the elements that failed the predicate **Examples** ```kex Set.from([1, 2, 3]).reject { |x| x > 1 } # => Set(1) ``` #### `showValue` (from Showable) ```kex showValue : String ``` Renders the set as `Set(...)` in element order, rather than exposing the record layout behind it. **Returns**: the rendered set **Examples** ```kex Set.from([2, 1]).showValue # => "Set(1, 2)" ``` #### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the set has no elements. The `Blankable` view of `empty?`, so generic code can test a set the same way it tests a string. **Returns**: `true` for the empty set **Examples** ```kex Set.empty.blank? # => true Set.from([1]).blank? # => false ``` ### From [`Enumerable`](../enumerable.md#trait-enumerable) - [`mapIndexed`](../enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`flatMap`](../enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](../enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](../enumerable.md#trait-foldable) - [`each`](../enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](../enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](../enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](../enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](../enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. ### From [`Monoid`](../algebra.md#trait-monoid) - [`repeat`](../algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Showable`](../kex.md#trait-showable) - [`to`](../kex.md#showable-to) — ## record `UnorderedSet` A set backed by a map from each member to `true`; its keys ARE the elements. Build one with `UnorderedSet.from`. Iteration order is whatever the map hands back, so use `items.sort` when you need a stable order. ```kex UnorderedSet.from([3, 1, 2]).contains?(2) # => true ``` **Fields** - `slots` : {A: [Bool](../truthyable.md#make-bool)} (optional) Implements [`Enumerable`](../enumerable.md#trait-enumerable), [`Foldable`](../enumerable.md#trait-foldable), [`Monoid`](../algebra.md#trait-monoid), [`Showable`](../kex.md#trait-showable), [`Blankable`](../blankable.md#trait-blankable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: B, f: (B -> A -> B)) -> B ``` Folds over the elements. The order is whatever the underlying map hands back: unspecified, and not to be relied on. Use a `Set` when the order of the fold matters. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each element **Returns**: the final accumulator **Examples** ```kex UnorderedSet.from([1, 2, 3]).reduce(0) { |sum, x| sum + x } # => 6 ``` #### `identity` (from Monoid) ```kex identity : UnorderedSet ``` The empty unordered set: the `Monoid` identity. **Returns**: the empty set #### `combine` (from Monoid) ```kex combine(other: UnorderedSet) -> UnorderedSet ``` Combines two sets by union. The `Monoid` operation. **Parameters** - `other` — the set to combine with **Returns**: every element of either set #### `contains?` ```kex contains?(value: A) -> Bool ``` Returns `true` when `value` is a member. This is the operation the flavour exists for: a map lookup, with no ordering to maintain. **Parameters** - `value` — the element to look for **Returns**: `true` when it is present **Examples** ```kex UnorderedSet.from([1, 2, 3]).contains?(2) # => true UnorderedSet.from([1, 2, 3]).contains?(9) # => false ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of distinct elements. **Returns**: the number of elements **Examples** ```kex UnorderedSet.from([1, 1, 2]).count # => 2 ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the set has no elements. **Returns**: `true` for the empty set **Examples** ```kex UnorderedSet.empty.empty? # => true ``` #### `items` ```kex items : [A] ``` Returns the elements as a list, in unspecified order. Sort the result when the order has to be stable: a test, a rendering, a comparison. **Returns**: the elements **Examples** ```kex UnorderedSet.from([3, 1, 2]).items.sort # => [1, 2, 3] ``` #### `add` ```kex add(value: A) -> UnorderedSet ``` Returns a new set with `value` added. Adding an existing member changes nothing. Use `add!` to rebind the receiver variable. **Parameters** - `value` — the element to add **Returns**: a set including `value` **Examples** ```kex UnorderedSet.from([1]).add(2).count # => 2 UnorderedSet.from([1]).add(1).count # => 1 ``` #### `delete` ```kex delete(value: A) -> UnorderedSet ``` Returns a new set without `value`. Removing a non-member changes nothing. Use `delete!` to rebind the receiver variable. **Parameters** - `value` — the element to remove **Returns**: a set without `value` **Examples** ```kex UnorderedSet.from([1, 2]).delete(1).count # => 1 ``` #### `union` ```kex union(other: UnorderedSet) -> UnorderedSet ``` Returns every element of either set. **Parameters** - `other` — the other set **Returns**: the union **Examples** ```kex UnorderedSet.from([1, 2]).union(UnorderedSet.from([2, 3])).count # => 3 ``` #### `intersect` ```kex intersect(other: UnorderedSet) -> UnorderedSet ``` Returns the elements both sets have. **Parameters** - `other` — the other set **Returns**: the intersection **Examples** ```kex UnorderedSet.from([1, 2, 3]).intersect(UnorderedSet.from([2, 3, 4])).items.sort # => [2, 3] ``` #### `difference` ```kex difference(other: UnorderedSet) -> UnorderedSet ``` Returns the elements of this set that `other` does not have. **Parameters** - `other` — the set to subtract **Returns**: the elements only this set has **Examples** ```kex UnorderedSet.from([1, 2, 3]).difference(UnorderedSet.from([2])).items.sort # => [1, 3] ``` #### `symmetricDifference` ```kex symmetricDifference(other: UnorderedSet) -> UnorderedSet ``` Returns the elements in exactly one of the two sets. **Parameters** - `other` — the other set **Returns**: the symmetric difference **Examples** ```kex UnorderedSet.from([1, 2]).symmetricDifference(UnorderedSet.from([2, 3])).items.sort # => [1, 3] ``` #### `subset?` ```kex subset?(other: UnorderedSet) -> Bool ``` Returns `true` when every element of this set is also in `other`. **Parameters** - `other` — the candidate superset **Returns**: `true` when this set is contained in `other` **Examples** ```kex UnorderedSet.from([1, 2]).subset?(UnorderedSet.from([1, 2, 3])) # => true ``` #### `superset?` ```kex superset?(other: UnorderedSet) -> Bool ``` Returns `true` when this set has every element of `other`. **Parameters** - `other` — the candidate subset **Returns**: `true` when this set contains all of `other` **Examples** ```kex UnorderedSet.from([1, 2, 3]).superset?(UnorderedSet.from([1, 2])) # => true ``` #### `disjoint?` ```kex disjoint?(other: UnorderedSet) -> Bool ``` Returns `true` when the two sets share no element. **Parameters** - `other` — the other set **Returns**: `true` when the intersection is empty **Examples** ```kex UnorderedSet.from([1, 2]).disjoint?(UnorderedSet.from([3])) # => true ``` #### `+` ```kex +(other: UnorderedSet) -> UnorderedSet +(other: [A]) -> UnorderedSet ``` Unions with another unordered set, or with a plain list. **Parameters** - `other` — the elements to add **Returns**: the union **Examples** ```kex (UnorderedSet.from([1, 2]) + [3]).count # => 3 ``` #### `-` ```kex -(other: UnorderedSet) -> UnorderedSet -(other: [A]) -> UnorderedSet ``` Removes another unordered set's elements, or a plain list's. **Parameters** - `other` — the elements to remove **Returns**: the difference **Examples** ```kex (UnorderedSet.from([1, 2, 3]) - [2]).count # => 2 ``` #### `map` (from Enumerable) ```kex map(f: (A -> B)) -> UnorderedSet ``` Applies `f` to every element and returns an unordered set of the results. Elements that map to the same value collapse into one. **Parameters** - `f` — applied to each element **Returns**: the distinct results **Examples** ```kex UnorderedSet.from([1, 2, 3]).map { |x| x * 2 }.items.sort # => [2, 4, 6] ``` #### `filter` (from Enumerable) ```kex filter(pred: (A -> Bool)) -> UnorderedSet ``` Returns a new unordered set with only the elements `pred` accepts. **Parameters** - `pred` — the test applied to each element **Returns**: the matching elements **Examples** ```kex UnorderedSet.from([1, 2, 3]).filter { |x| x > 1 }.items.sort # => [2, 3] ``` #### `reject` ```kex reject(pred: (A -> Bool)) -> UnorderedSet ``` Returns a new unordered set without the elements `pred` accepts. **Parameters** - `pred` — the test applied to each element **Returns**: the elements that failed the predicate **Examples** ```kex UnorderedSet.from([1, 2, 3]).reject { |x| x > 1 }.items # => [1] ``` #### `showValue` (from Showable) ```kex showValue : String ``` Renders the set as `UnorderedSet(...)`. The elements are printed in sorted order where they allow it, so the rendering of a value is not at the mercy of map internals. **Returns**: the rendered set **Examples** ```kex UnorderedSet.from([2, 1]).showValue # => "UnorderedSet(1, 2)" ``` #### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the set has no elements. The `Blankable` view of `empty?`. **Returns**: `true` for the empty set **Examples** ```kex UnorderedSet.empty.blank? # => true ``` ### From [`Enumerable`](../enumerable.md#trait-enumerable) - [`mapIndexed`](../enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`flatMap`](../enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](../enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](../enumerable.md#trait-foldable) - [`each`](../enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](../enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](../enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](../enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](../enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. ### From [`Monoid`](../algebra.md#trait-monoid) - [`repeat`](../algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Showable`](../kex.md#trait-showable) - [`to`](../kex.md#showable-to) — ## module `Data.Set` Constructors for the sorted `Set`. ### `from` ```kex from(items: [A]) -> Set ``` Builds a sorted set from a list, discarding duplicates. This is the normal way to make a `Set`. Deduplication goes through a map rather than `List.uniq`: map keys are unique under exactly the structural equality a set wants, and each element costs one insertion instead of a scan of everything kept so far. **Parameters** - `items` — the elements, in any order and with any duplicates **Returns**: the distinct elements, sorted **Examples** ```kex Set.from([3, 1, 2, 3]) # => Set(1, 2, 3) Set.from("hello".chars) # => Set(e, h, l, o) ``` _Removing duplicates from a list_ ```kex Set.from(["b", "a", "b"]).items # => ["a", "b"] ``` ### `empty` (constant) ```kex empty : Set ``` The set with no elements. Also the `Monoid` identity, so `s.combine(Set.empty)` is `s`. **Examples** ```kex Set.empty.empty? # => true Set.empty.add(1) # => Set(1) ``` ## module `Data.UnorderedSet` Constructors for the hash-backed `UnorderedSet`. ### `from` ```kex from(items: [A]) -> UnorderedSet ``` Builds an unordered set from a list, discarding duplicates. Nothing is sorted, so unlike `Set.from` this does not require the elements to be `Orderable`. **Parameters** - `items` — the elements, in any order and with any duplicates **Returns**: the distinct elements **Examples** ```kex UnorderedSet.from([3, 1, 2, 3]).count # => 3 ``` _A fast membership test over a large list_ ```kex let stopWords = UnorderedSet.from(["the", "a", "of"]) words.reject { |w| stopWords.contains?(w) } ``` ### `empty` (constant) ```kex empty : UnorderedSet ``` The unordered set with no elements. Also the `Monoid` identity. **Examples** ```kex UnorderedSet.empty.empty? # => true ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: data/stack.kex title: Data.Stack entities: - { kind: module, name: "Data" } --- # Data.Stack ## module `Data` A last-in-first-out stack. Opt-in: nothing here is in scope until `using Data.Stack`. ```kex using Data.Stack ``` Elements are stored top first, so `push`, `pop` and `peek` are all list-head operations: none of them pay for the size of the stack. `items` reverses that internal order, so it reads bottom-to-top, the order you would have pushed them in: ```kex let s = Stack.from([1, 2, 3]) s.peek # => Just(3) s.push(4).items # => [1, 2, 3, 4] s.pop # => Just((3, Stack(1, 2))) Stack.empty.pop # => None ``` Updates return a new stack rather than changing the receiver. `push!` and `pop!` come free from the `!` rebinding form, the same as `add!`/`delete!` do for `Data.Set`: they build a new stack and rebind the receiver variable rather than modifying anything in place. ## record `Stack` A stack of elements, held top first. Build one with `Stack.from` rather than by hand: the record literal takes elements in storage order (top first), which reads backwards from the `items` a caller normally thinks in. ```kex Stack.from([1, 2, 3]).items # => [1, 2, 3] ``` **Fields** - `elements` : [A] (optional) Implements [`Enumerable`](../enumerable.md#trait-enumerable), [`Foldable`](../enumerable.md#trait-foldable), [`Monoid`](../algebra.md#trait-monoid), [`Showable`](../kex.md#trait-showable), [`Blankable`](../blankable.md#trait-blankable). ### Functions #### `reduce` (from Enumerable, Foldable) ```kex reduce(acc: B, f: (B -> A -> B)) -> B ``` Folds from the top down to the bottom. This is `Stack`'s `Enumerable`/`Foldable` primitive. **Parameters** - `acc` — the initial accumulator - `f` — combines the accumulator with each element **Returns**: the final accumulator **Examples** ```kex Stack.from([1, 2, 3]).reduce(0) { |sum, x| sum + x } # => 6 ``` #### `identity` (from Monoid) ```kex identity : Stack ``` The empty stack: the `Monoid` identity. **Returns**: the empty stack #### `combine` (from Monoid) ```kex combine(other: Stack) -> Stack ``` Combines two stacks by pushing the argument's elements on top of this one, top element last. **Parameters** - `other` — the stack to push on top **Returns**: this stack with `other` stacked above it **Examples** ```kex Stack.from([1, 2]).combine(Stack.from([3, 4])).items # => [1, 2, 3, 4] ``` #### `items` ```kex items : [A] ``` Returns the elements bottom-to-top: the order you would have pushed them in. **Returns**: the elements, bottom first **Examples** ```kex Stack.from([1, 2, 3]).items # => [1, 2, 3] ``` #### `push` ```kex push(value: A) -> Stack ``` Returns a new stack with `value` pushed on top. Use `push!` to rebind the receiver variable. **Parameters** - `value` — the element to push **Returns**: a stack with `value` on top **Examples** ```kex Stack.from([1, 2]).push(3).items # => [1, 2, 3] ``` #### `pop` ```kex pop : (A, Stack)? ``` Returns the top element and the stack without it, wrapped in `Just`, or `None` for an empty stack. Use `pop!` to rebind the receiver variable. **Returns**: the top element and the rest, or `None` **Examples** ```kex Stack.from([1, 2, 3]).pop # => Just((3, Stack(1, 2))) Stack.empty.pop # => None ``` #### `peek` ```kex peek : A? ``` Returns the top element wrapped in `Just`, or `None` for an empty stack. **Returns**: the top element, or `None` **Examples** ```kex Stack.from([1, 2, 3]).peek # => Just(3) Stack.empty.peek # => None ``` #### `count` (from Foldable) ```kex count : Integer ``` Returns the number of elements. **Returns**: the number of elements **Examples** ```kex Stack.from([1, 2, 3]).count # => 3 ``` #### `empty?` ```kex empty? : Bool ``` Returns `true` when the stack has no elements. **Returns**: `true` for the empty stack **Examples** ```kex Stack.empty.empty? # => true Stack.from([1]).empty? # => false ``` #### `+` ```kex +(other: Stack) -> Stack +(other: [A]) -> Stack ``` Pushes another stack's elements, or a plain list's, on top. The list form reads bottom-to-top, the same as `Stack.from`: the last element of the list ends up on top. **Parameters** - `other` — the elements to push **Returns**: this stack with `other` pushed above it **Examples** ```kex Stack.from([1, 2]) + [3, 4] # => Stack(1, 2, 3, 4) Stack.from([1, 2]) + Stack.from([3]) # => Stack(1, 2, 3) ``` #### `showValue` (from Showable) ```kex showValue : String ``` Renders the stack as `Stack(...)`, bottom-to-top. **Returns**: the rendered stack **Examples** ```kex Stack.from([1, 2, 3]).showValue # => "Stack(1, 2, 3)" ``` #### `blank?` (from Blankable) ```kex blank? : Bool ``` Returns `true` when the stack has no elements. The `Blankable` view of `empty?`. **Returns**: `true` for the empty stack **Examples** ```kex Stack.empty.blank? # => true Stack.from([1]).blank? # => false ``` ### From [`Enumerable`](../enumerable.md#trait-enumerable) - [`map`](../enumerable.md#enumerable-map) — Applies `f` to each item and collects the results into a list. - [`mapIndexed`](../enumerable.md#enumerable-mapindexed) — Applies `f` to each item and its 0-based position, and collects the results into a list. - [`filter`](../enumerable.md#enumerable-filter) — Returns the items for which `pred` answers `true`. - [`flatMap`](../enumerable.md#enumerable-flatmap) — Applies `f` to each item, expecting a list back, and concatenates the results into one flat list. - [`collect`](../enumerable.md#enumerable-collect) — Applies `f` to each item, expecting an `Optional` back, and returns the values that were present: unwrapped. ### From [`Foldable`](../enumerable.md#trait-foldable) - [`each`](../enumerable.md#foldable-each) — Calls `f` with each item, for its side effects. - [`eachIndexed`](../enumerable.md#foldable-eachindexed) — Calls `f` with each item and its 0-based position, for its side effects. - [`all?`](../enumerable.md#foldable-all?) — Returns `true` when every item satisfies `pred`. - [`any?`](../enumerable.md#foldable-any?) — Returns `true` when at least one item satisfies `pred`. - [`find`](../enumerable.md#foldable-find) — Returns the first item satisfying `pred`, or `None` when nothing does. ### From [`Monoid`](../algebra.md#trait-monoid) - [`repeat`](../algebra.md#monoid-repeat) — Combines this value with itself `n` times. ### From [`Showable`](../kex.md#trait-showable) - [`to`](../kex.md#showable-to) — ## module `Data.Stack` Constructors for `Stack`. ### `from` ```kex from(items: [A]) -> Stack ``` Builds a stack from a list, read bottom-to-top: the last element is on top. **Parameters** - `items` — the elements, bottom first **Returns**: the stack, with `items`'s last element on top **Examples** ```kex Stack.from([1, 2, 3]).peek # => Just(3) ``` ### `empty` (constant) ```kex empty : Stack ``` The stack with no elements. Also the `Monoid` identity. **Examples** ```kex Stack.empty.empty? # => true ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: kex/ast.kex title: Kex.AST entities: - { kind: module, name: "Kex.AST" } --- # Kex.AST ## module `Kex.AST` Parses Kex source code into a structured AST, at run time. Opt-in: nothing here is in scope until `using Kex.AST`. This is the entry point for tools that read Kex source: linters, formatters, documentation generators, code search. The AST includes module definitions, function signatures, type/record definitions, traits, make blocks, and the doc-comments extracted from `#` lines, which is how the standard library's own documentation is generated. ```kex using Kex.AST main do match Kex.AST.parseFile("src/main.kex") do Ok(program) => IO.printLine("${program.items.count} top-level items") Error(e) => IO.printError(e.message) end end ``` Everything answers a `Result`, so a source file that does not parse is a value you handle rather than an exception. ### `parse` ```kex parse(source: String) -> Result parse(source: String, filename: String) -> Result ``` Parses Kex source text into a structured AST. Pass `filename` when you have one: it is what appears in every `Location` and in the error message, so diagnostics can point at a real file. **Parameters** - `source` — the Kex source text - `filename` — the name to report locations against; optional **Returns**: the parsed program, or why it failed **Examples** ```kex Kex.AST.parse("let double(n: Integer) -> Integer = n * 2\n") # => Ok(Program { schemaVersion: 2, items: [...] }) Kex.AST.parse("let x =").error? # => true ``` _Counting a file's top-level items_ ```kex Kex.AST.parse(source, "main.kex").map { |p| p.items.count } ``` ### `parseFile` ```kex parseFile(path: FS.FilePath) -> Result ``` Reads a file and parses it, reporting locations against its path. A file that cannot be read is an `Error` like one that cannot be parsed. **Parameters** - `path` — the file to read and parse **Returns**: the parsed program, or why it failed **Examples** ```kex match Kex.AST.parseFile("src/main.kex") do Ok(program) => IO.printLine("${program.items.count} items") Error(e) => IO.printError(e.message) end ``` _Parsing every source in a directory_ ```kex FS.Directory.files(dir) .or([]) .filter { |f| FS.Path.extension(f) == ".kex" } .map { |f| Kex.AST.parseFile(FS.Path.join(dir, f)) } ``` ### `parseType` ```kex parseType(source: String) -> Result ``` Parses a type expression on its own, without a surrounding program. Use it to read a type written in data: a signature in a config file, a type named on a command line. `typeRefText` renders the result back. **Parameters** - `source` — the type expression **Returns**: the parsed type, or why it failed **Examples** ```kex Kex.AST.parseType("[Integer]").map { |t| Kex.AST.typeRefText(t) } # => Ok("[Integer]") Kex.AST.parseType("Map").map { |t| Kex.AST.typeRefText(t) } # => Ok("Map") ``` ### `parseExpression` ```kex parseExpression(source: String) -> Result ``` Parses a single expression on its own, without a surrounding program. This gives you the expression's SHAPE. To evaluate one instead, use `Evaluator.runExpression`. **Parameters** - `source` — the expression text **Returns**: the parsed expression, or why it failed **Examples** ```kex Kex.AST.parseExpression("1 + 2").ok? # => true Kex.AST.parseExpression("1 +").ok? # => false ``` ### `parseSyntax` ```kex parseSyntax(source: String) -> Result ``` Parses Kex source into its lossless syntax tree. **Parameters** - `source` — the Kex source text **Returns**: the tree rooted at `Program`, or why it failed **Examples** ```kex Kex.AST.parseSyntax("let x = 1\n").map { |tree| tree.kind } # => Ok("Program") Kex.AST.parseSyntax("let x =").error? # => true ``` ### `toSource` ```kex toSource(node: SyntaxNode) -> String ``` Reprints a syntax tree as the source it was parsed from, byte for byte. Every node prints its own tokens and its nested nodes in order, never a slice of the original text, so a rearranged tree prints the rearranged program, its comments moving with it. **Parameters** - `node` — a tree, or any node in one **Returns**: the source the node covers, trivia included **Examples** ```kex let source = "let x = 1 # one\n" Kex.AST.parseSyntax(source).map { |tree| Kex.AST.toSource(tree) } # => Ok(source) ``` ### `elementSource` ```kex elementSource(element: SyntaxElement) -> String ``` The source of one child of a node: a token's trivia and text, or a nested node reprinted. **Parameters** - `element` — the child **Returns**: its source text ### `commentsBefore` ```kex commentsBefore(parent: SyntaxNode, index: Integer) -> [String] ``` The comments written on their own lines directly above the child at `index`: the ones that belong to it, move with it when a formatter moves it, and hold a `# kex:disable-next-line` meant for it. A comment at the end of the previous line trails that line instead, and is not included. **Parameters** - `parent` — the node holding the child - `index` — the child's position in `parent.children` **Returns**: the comment lines, top to bottom, each starting with `#` **Examples** ```kex let tree = Kex.AST.parseSyntax("let x = 1 # x\n# about y\nlet y = 2\n").try Kex.AST.commentsBefore(tree, 3) # => ["# about y"] ``` ### `blankLinesBefore` ```kex blankLinesBefore(parent: SyntaxNode, index: Integer) -> Integer ``` How many blank lines separate the child at `index` from what precedes it. Kept as a count, not a flag: a formatter preserves one blank line between declarations and collapses longer runs, which needs to know how many there were. **Parameters** - `parent` — the node holding the child - `index` — the child's position in `parent.children` **Returns**: the number of blank lines directly above the child **Examples** ```kex let tree = Kex.AST.parseSyntax("let x = 1\n\n\nlet y = 2\n").try Kex.AST.blankLinesBefore(tree, 4) # => 2 ``` ### `linesBefore` ```kex linesBefore(parent: SyntaxNode, index: Integer) -> [String] ``` The whole lines between the child at `index` and the code before it, as the trivia of the `Newline` tokens that end them. The newline that ends the previous line of code is not one of them: what sits in front of it trails that code. **Parameters** - `parent` — the node holding the child - `index` — the child's position in `parent.children` **Returns**: the lines, top to bottom, without their newlines ### `typeRefText` ```kex typeRefText(typeRef: TypeRef) -> String ``` Renders a `TypeRef` back to the way it is written in source. A list reads as `[Integer]`, a map as `{String: Integer}`, an optional as `String?`: the spelling a reader would recognise, not the constructor tree behind it. **Parameters** - `typeRef` — the parsed type **Returns**: the type, as source **Examples** ```kex Kex.AST.parseType("[Integer]").map { |t| Kex.AST.typeRefText(t) } # => Ok("[Integer]") Kex.AST.parseType("String?").map { |t| Kex.AST.typeRefText(t) } # => Ok("String?") ``` ### `patternRefText` ```kex patternRefText(patternRef: PatternRef) -> String ``` Renders a `PatternRef` back to the way it is written in source. **Parameters** - `patternRef` — the parsed pattern **Returns**: the pattern, as source **Examples** ```kex Kex.AST.patternRefText(WildcardPattern) # => "_" Kex.AST.patternRefText(BindPattern("n")) # => "n" ``` ### `patternFieldText` ```kex patternFieldText(name, _, _) -> String ``` ### `referenceText` ```kex referenceText(reference: TypeRef | PatternRef) -> String ``` Renders either a type or a pattern back to source. The one call to reach for when a node may carry either: it dispatches to `typeRefText` or `patternRefText` as appropriate. **Parameters** - `reference` — the parsed node **Returns**: the node, as source **Examples** ```kex Kex.AST.referenceText(WildcardPattern) # => "_" Kex.AST.referenceText(AnyType) # => "Any" ``` ## record `Location` Where in a source file something appeared. Line and column are 1-based, for reporting to a person; the offsets are 0-based byte positions, for slicing the source. **Fields** - `file` : [String](../string.md#make-string) - `line` : [Integer](../number.md#make-integer) - `column` : [Integer](../number.md#make-integer) - `startOffset` : [Integer](../number.md#make-integer) - `endOffset` : [Integer](../number.md#make-integer) ## record `Program` A parsed source file: its schema version, and its top-level items. **Fields** - `schemaVersion` : [Integer](../number.md#make-integer) - `items` : [Node] ## record `ParseError` Why a source file could not be parsed. **Fields** - `message` : [String](../string.md#make-string) - `location` : [Location](#record-kex-ast-location)? ## record `SyntaxToken` One token of a lossless syntax tree, exactly as written. Where `parse` gives a program's meaning, `parseSyntax` gives its text: nothing is normalised or dropped, so `toSource` reprints the file byte for byte. Use it when building a formatter or a linter. **Fields** - `kind` : [String](../string.md#make-string) - `text` : [String](../string.md#make-string) - `trivia` : [String](../string.md#make-string) ## type `SyntaxElement` One child of a `SyntaxNode`: a token, or a nested node. **Variants** - `TokenElement(SyntaxToken)` - `NodeElement(SyntaxNode)` ## record `SyntaxNode` A declaration, expression, pattern or type, holding its own tokens and nested nodes in source order. ```kex let tree = Kex.AST.parseSyntax("# the answer\nlet x = 42\n").try tree.kind # => "Program" Kex.AST.toSource(tree) # => "# the answer\nlet x = 42\n" ``` **Fields** - `kind` : [String](../string.md#make-string) - `children` : [[SyntaxElement](#type-kex-ast-syntaxelement)] ## type `TypeRef` A type as it was written in source. `typeRefText` renders one back to the source spelling. **Variants** - `NamedType(String, [TypeRef])` - `FunctionType([TypeRef], TypeRef)` - `TupleType([TypeRef])` - `ListType(TypeRef)` - `MapType(TypeRef, TypeRef)` - `UnionType([TypeRef])` - `IntersectionType([TypeRef])` - `RecordType([(String, TypeRef)])` - `NullableType(TypeRef)` - `BlockType(TypeRef)` - `ParameterBlockType([TypeRef], TypeRef)` - `AtomType(String)` - `TypeQuery(String, Expression)` - `TypeVar(String)` - `AnyType` - `NoneType` ## type `PatternRef` Structured representation of patterns. Pattern nodes describe what a declaration or match arm accepts; they do not contain runtime values. A linter can distinguish a wildcard from a binding, for example, without reparsing source text. **Examples** _Finding catch-all match arms_ ```kex let catchAll? = Kex.AST.patternRefText(pattern) == "_" ``` **Variants** - `BindPattern(String)` - `LiteralPattern(String)` - `ConstructorPattern(String, [PatternRef])` - `TuplePattern([PatternRef])` - `ListPattern([PatternRef], PatternRef?)` - `RecordPattern(String?, [PatternField])` - `RangePattern(PatternRef, PatternRef)` - `ThisPattern(PatternRef)` - `WildcardPattern` ## record `PatternField` One field inside a record or map-shaped pattern. `pattern` is `None` for shorthand such as `{ name }`. `stringKey` keeps `{"name": value}` distinct from the atom-key spelling `{ name: value }`. **Fields** - `name` : [String](../string.md#make-string) - `pattern` : [PatternRef](#type-kex-ast-patternref)? - `stringKey` : [Bool](../truthyable.md#make-bool) ## type `Expression` Structured representation of expression AST nodes. Expressions retain syntax-level distinctions that matter to tools: a receiver call is not flattened into a generic call, `var` is distinct from `let`, and a trailing `if` remains recognizable. Walk these constructors when writing a linter or code search; use `Evaluator` when the goal is to execute an expression rather than inspect it. **Variants** - `LitInt(Int)` - `LitFloat(Float)` - `LitChar(Int)` - `LitString(String)` - `InterpolatedString([String], [Expression])` - `LitBool(Bool)` - `LitAtom(String)` - `LitNone` - `Identifier(String)` - `This` - `BinaryOp(Expression, String, Expression)` - `UnaryOp(String, Expression)` - `Call(Expression, [Expression], [NamedArgument], Expression?)` - `TaggedLiteral(String, [String], [Expression])` - `MethodCall(Expression, String, [Expression], [NamedArgument], Expression?, Bool, Bool, TypeRef?)` - `If(Expression, PatternRef?, [Expression], [ElseIf], [Expression]?)` - `Match(Expression, String?, [MatchArm])` - `Receive(String?, [MatchArm], Expression?, Expression?)` - `ListLit([Expression], Expression?)` - `MapLit([MapItem])` - `RecordLit(String, [RecordField])` - `TupleLit([Expression])` - `Block([Expression])` - `Lambda([LambdaParam], [Expression], TypeRef?, RescueInfo?)` - `Let(PatternRef, TypeRef?, Expression)` - `Var(String, TypeRef?, Expression)` - `Assign(String, Expression)` - `Return(Expression)` - `Break` - `Next` - `Spawn([Expression])` - `Try(Expression)` - `Spread(Expression)` - `TrailingIf(Expression, Expression)` - `ThenElse(Expression, Expression, Expression)` - `ShorthandLambda(String, [Expression], Bool)` - `CurryPlaceholder` - `Curry(String, String?, Bool, [[Expression]])` - `Using(String, String?, [String], [String], [Expression])` - `With(String, Expression, [Expression])` - `GeneratedDeclaration(Expression, GeneratedTemplate)` - `ErrorExpression(String)` - `Trying([Expression], RescueInfo)` - `While(Expression, [Expression])` - `Loop([String], [Expression])` - `RangeLit(Expression, Expression)` ## record `NamedArgument` One `name: value` argument at a call site. **Fields** - `name` : [String](../string.md#make-string) - `value` : [Expression](#type-kex-ast-expression) ## record `MatchArm` One arm of `match`, `receive`, or `rescue`. Multiple `patterns` are the comma-separated alternatives on the left of the arrow. `guard` is absent when the arm has no `when` condition. **Fields** - `patterns` : [[PatternRef](#type-kex-ast-patternref)] - `guard` : [Expression](#type-kex-ast-expression)? - `body` : [Expression](#type-kex-ast-expression) ## record `LambdaParam` One lambda parameter and its optional source annotation. **Fields** - `name` : [String](../string.md#make-string) - `type` : [TypeRef](#type-kex-ast-typeref)? ## record `RescueInfo` The structured recovery clauses attached to a function or expression. Named rescue arms live in `arms`; a catch-all rescue keeps its optional binding and body separately. `inlineReturn` represents the compact rescue form rather than inventing a synthetic block. **Fields** - `arms` : [[MatchArm](#record-kex-ast-matcharm)] - `catchAllName` : [String](../string.md#make-string)? - `catchAllBody` : [[Expression](#type-kex-ast-expression)] - `inlineReturn` : [Expression](#type-kex-ast-expression)? ## record `ElseIf` One `elif` branch, in source order. **Fields** - `condition` : [Expression](#type-kex-ast-expression) - `body` : [[Expression](#type-kex-ast-expression)] ## type `MapItem` One entry in a map literal: either a key/value pair or `...spread`. **Variants** - `MapEntry(Expression, Expression)` - `MapSpread(Expression)` ## record `RecordField` One explicitly initialized field in a record literal. **Fields** - `name` : [String](../string.md#make-string) - `value` : [Expression](#type-kex-ast-expression) ## type `GeneratedTemplate` A declaration template whose name (and, for a make block, target) is computed by a `compiled do` expression. Tools normally encounter this only while inspecting metaprogramming code. After expansion, generated declarations appear as ordinary +Node+s. **Variants** - `GeneratedNode(Node)` - `GeneratedMake(GeneratedMakeInfo)` ## record `GeneratedMakeInfo` The fixed portion of a generated `make` declaration. **Fields** - `isFinal` : [Bool](../truthyable.md#make-bool) - `implements` : [[TypeRef](#type-kex-ast-typeref)] - `body` : [[CompiledItem](#type-kex-ast-compileditem)] - `location` : [Location](#record-kex-ast-location) ## record `MainInfo` The program entry point, including documentation and recovery clauses. **Fields** - `doc` : [String](../string.md#make-string)? - `params` : [[ParamInfo](#record-kex-ast-paraminfo)] - `body` : [[Expression](#type-kex-ast-expression)] - `rescueInfo` : [RescueInfo](#record-kex-ast-rescueinfo)? - `location` : [Location](#record-kex-ast-location) ## record `ParamInfo` One declared function parameter. `name` is absent for a destructuring parameter; `pattern` preserves that destructuring shape. `hasDefault` records whether an initializer appeared. **Fields** - `name` : [String](../string.md#make-string)? - `pattern` : [PatternRef](#type-kex-ast-patternref)? - `type` : [TypeRef](#type-kex-ast-typeref)? - `hasDefault` : [Bool](../truthyable.md#make-bool) ## record `ClauseInfo` One clause of a function, including its patterns and body. Multi-clause functions place all clauses in one `FunctionInfo`, preserving source order so tooling can reason about which pattern is tried first. **Fields** - `params` : [[ParamInfo](#record-kex-ast-paraminfo)] - `body` : [[Expression](#type-kex-ast-expression)] - `returnType` : [TypeRef](#type-kex-ast-typeref)? - `rescueInfo` : [RescueInfo](#record-kex-ast-rescueinfo)? - `hasParamList` : [Bool](../truthyable.md#make-bool) ## record `FunctionInfo` A named function and all of its pattern-matching clauses. `doc` contains the normalized `#` comment immediately attached to the declaration. Documentation generators can therefore share the same parsed structure as linters instead of scanning comments independently. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `isFoul` : [Bool](../truthyable.md#make-bool) - `predicate` : [Bool](../truthyable.md#make-bool) - `clauses` : [[ClauseInfo](#record-kex-ast-clauseinfo)] - `location` : [Location](#record-kex-ast-location) ## record `AnnotationInfo` A standalone function type signature. `implicitThis` distinguishes `:>` receiver signatures from module-level `:` signatures without making a tool inspect punctuation in the original source. **Fields** - `name` : [String](../string.md#make-string) - `type` : [TypeRef](#type-kex-ast-typeref) - `doc` : [String](../string.md#make-string)? - `implicitThis` : [Bool](../truthyable.md#make-bool) - `location` : [Location](#record-kex-ast-location) ## record `VariantInfo` One constructor of an algebraic data type. **Fields** - `name` : [String](../string.md#make-string) - `fields` : [[TypeRef](#type-kex-ast-typeref)] ## record `TypeInfo` A type alias or algebraic data type declaration. `variants` is present for an ADT and absent for an alias or abstract type. `parents` preserves declared bounds and inherited type relationships. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `typeParams` : [[String](../string.md#make-string)] - `parents` : [[TypeRef](#type-kex-ast-typeref)] - `variants` : [[VariantInfo](#record-kex-ast-variantinfo)]? - `location` : [Location](#record-kex-ast-location) ## record `FieldInfo` One field declared by a record type. **Fields** - `name` : [String](../string.md#make-string) - `type` : [TypeRef](#type-kex-ast-typeref) - `hasDefault` : [Bool](../truthyable.md#make-bool) ## record `RecordInfo` A record declaration with fields in source order. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `typeParams` : [[String](../string.md#make-string)] - `fields` : [[FieldInfo](#record-kex-ast-fieldinfo)] - `location` : [Location](#record-kex-ast-location) ## record `TraitInfo` A trait declaration and the signatures or default functions in its body. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `typeParams` : [[String](../string.md#make-string)] - `body` : [Node] - `location` : [Location](#record-kex-ast-location) ## record `MakeInfo` A `make` implementation block. `target` is the receiver type, `implements` lists explicit traits, and `body` retains functions and visibility sections in declaration order. **Fields** - `target` : [TypeRef](#type-kex-ast-typeref) - `doc` : [String](../string.md#make-string)? - `isFinal` : [Bool](../truthyable.md#make-bool) - `implements` : [[TypeRef](#type-kex-ast-typeref)] - `body` : [Node] - `location` : [Location](#record-kex-ast-location) ## record `PragmaInfo` A compiler pragma and its optional value. **Fields** - `name` : [String](../string.md#make-string) - `value` : [String](../string.md#make-string)? - `location` : [Location](#record-kex-ast-location) ## record `ModuleInfo` A module and its declarations in source order. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `items` : [Node] - `location` : [Location](#record-kex-ast-location) ## record `ConstantInfo` A named constant declaration. The AST reader never evaluates its value. **Fields** - `name` : [String](../string.md#make-string) - `doc` : [String](../string.md#make-string)? - `type` : [TypeRef](#type-kex-ast-typeref)? - `location` : [Location](#record-kex-ast-location) ## record `VisibilityInfo` A `public` or `private` section and the declarations it contains. **Fields** - `isPublic` : [Bool](../truthyable.md#make-bool) - `items` : [Node] - `location` : [Location](#record-kex-ast-location) ## record `UsingInfo` A `using` import, including aliasing, filters, and an optional scoped body. **Fields** - `moduleName` : [String](../string.md#make-string) - `alias` : [String](../string.md#make-string)? - `onlyNames` : [[String](../string.md#make-string)] - `exceptNames` : [[String](../string.md#make-string)] - `body` : [[Expression](#type-kex-ast-expression)] - `location` : [Location](#record-kex-ast-location) ## record `ExportInfo` An `export` declaration and its public-name filters. **Fields** - `moduleName` : [String](../string.md#make-string) - `alias` : [String](../string.md#make-string)? - `onlyNames` : [[String](../string.md#make-string)] - `exceptNames` : [[String](../string.md#make-string)] - `location` : [Location](#record-kex-ast-location) ## type `CompiledItem` One item inside a `compiled do` block before expansion. **Variants** - `CompiledNode(Node)` - `CompiledExpression(Expression)` ## record `CompiledInfo` A compile-time block and its declarations or expressions in source order. **Fields** - `items` : [[CompiledItem](#type-kex-ast-compileditem)] - `location` : [Location](#record-kex-ast-location) ## type `Node` Any top-level or declaration-level AST node. A source tool can match only the declarations it understands and leave the rest alone. The program's `schemaVersion` lets persisted consumers reject a tree whose possible node shapes have changed. **Examples** _Listing documented functions in a parsed file_ ```kex program.items.each do |node| match node do FunctionDef(info) if info.doc.present? => IO.printLine(info.name) _ => () end end ``` **Variants** - `ModuleDef(ModuleInfo)` - `FunctionDef(FunctionInfo)` - `TypeAnnotation(AnnotationInfo)` - `TypeDef(TypeInfo)` - `RecordDef(RecordInfo)` - `TraitDef(TraitInfo)` - `MakeDef(MakeInfo)` - `PragmaDef(PragmaInfo)` - `ConstantDef(ConstantInfo)` - `MainDef(MainInfo)` - `Visibility(VisibilityInfo)` - `UsingDef(UsingInfo)` - `ExportDef(ExportInfo)` - `Compiled(CompiledInfo)` ## type `TypeRef | PatternRef` Source-like conversion through the standard `to(String)` spelling. Useful in diagnostics: a tool can interpolate the type or pattern it found without manually dispatching between the two reference families. **Examples** _Building a lint message from a parsed annotation_ ```kex let written = reference.to(String).or("unknown") IO.warn("avoid the broad ${written} annotation") ``` ### `to` ```kex to(_) -> String? ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: net/dns.kex title: Net.DNS entities: - { kind: module, name: "Net.DNS" } --- # Net.DNS ## module `Net.DNS` Typed DNS lookup with explicit resolver ownership and bounded caching. Use `DNS.addresses` for an occasional hostname lookup. Own a `Resolver` when a service performs repeated lookups, needs cache statistics, or must query a particular nameserver. Resolver ownership makes both caching and cleanup visible instead of hiding process-wide state behind every lookup. ```kex using Net.DNS let resolver = Resolver.system.try let name = Name.parse("example.test").try let addresses = resolver.addresses(name).try resolver.close ``` ## record `Name` A validated DNS name with caller-facing and IDNA ASCII spellings. `display` keeps the readable form supplied by the caller; `ascii` is the wire-safe IDNA form used in DNS queries. Keeping both lets an error message say what the user typed without sending noncanonical labels to a resolver. **Fields** - `display` : [String](../string.md#make-string) - `ascii` : [String](../string.md#make-string) ## type `RecordType` Record families supported by `Resolver.lookup`. **Variants** - `A` - `AAAA` - `CNAME` - `MX` - `TXT` - `SRV` - `PTR` ## type `DNSSECStatus` DNSSEC state reported by the underlying resolver. Kex does not independently validate DNSSEC and therefore commonly reports `Indeterminate`. **Variants** - `Secure` - `Insecure` - `Bogus` - `Indeterminate` ## type `DNSRecord` Typed DNS resource records, preserving MX/SRV priorities and TXT chunks. **Variants** - `AddressRecord(Net.IP.Address)` - `CanonicalName(Name)` - `MailExchange(Integer, Name)` - `TextRecord([String])` - `ServiceRecord(Integer, Integer, Net.Port, Name)` - `PointerRecord(Name)` ## record `LookupResponse` Records and the reported DNSSEC state from one lookup. **Fields** - `records` : [[DNSRecord](#type-net-dns-dnsrecord)] - `dnssec` : [DNSSECStatus](#type-net-dns-dnssecstatus) ## record `CacheOptions` Bounds for a resolver-owned positive and negative cache. Positive answers honor their DNS TTL up to `maximumTtl`. Failed lookups are cached for `negativeTtl`, preventing a missing hostname from hammering the configured nameserver on every request. **Fields** - `entries` : [Integer](../number.md#make-integer) (optional) - `maximumTtl` : [Duration](../units.md#record-duration) (optional) - `negativeTtl` : [Duration](../units.md#record-duration) (optional) ## record `Nameserver` One DNS server used by a custom resolver. **Fields** - `address` : Net.IP.Address - `port` : Net.Port ## record `ResolverOptions` Isolated resolver configuration. An empty search list only queries the name as written; search domains are tried in order for single-label names. **Fields** - `cache` : [CacheOptions](#record-net-dns-cacheoptions) (optional) - `nameservers` : [[Nameserver](#record-net-dns-nameserver)] - `search` : [[Name](#record-net-dns-name)] (optional) - `retries` : [Integer](../number.md#make-integer) (optional) - `timeout` : [Duration](../units.md#record-duration) (optional) ## record `CacheStatistics` Lifetime cache counters. `clear` empties entries but keeps these counters. Compare `hits` with `misses` when tuning `entries` or TTL bounds. A rising `evictions` count means the resolver is seeing more distinct names than its cache can retain. **Fields** - `entries` : [Integer](../number.md#make-integer) - `hits` : [Integer](../number.md#make-integer) - `misses` : [Integer](../number.md#make-integer) - `negativeHits` : [Integer](../number.md#make-integer) - `evictions` : [Integer](../number.md#make-integer) ## type `Resolver` An opaque, process-safe resolver that owns its cache. ### Functions #### `addresses` ```kex addresses(name: Name) -> Result<[Net.IP.Address], NetError> ``` Resolves AAAA and A records, returning IPv6 addresses first. This is the convenient operation for connecting to a host. Use `lookup` when record type, TTL-related behavior, or DNSSEC status matters. **Parameters** - `name` — the validated hostname **Returns**: addresses or `Resolve` **Examples** _Resolving an application upstream_ ```kex let host = Name.parse("api.example.com").try let addresses = resolver.addresses(host).try ``` #### `lookup` ```kex lookup(kind: RecordType, name: Name) -> Result ``` Looks up one supported resource-record family. **Parameters** - `kind` — the requested family - `name` — the validated owner name **Returns**: typed records or `Resolve` **Examples** _Discovering a domain's mail exchangers_ ```kex let response = resolver.lookup(MX, Name.parse("example.com").try).try response.records.each { |record| IO.inspect(record) } ``` #### `clear` ```kex clear : Void ``` Empties cached entries without resetting lifetime counters. Existing statistics remain meaningful across a manual refresh, while the next lookup is forced back to DNS. **Examples** _Refreshing service discovery after configuration changes_ ```kex resolver.clear ``` #### `statistics` ```kex statistics : CacheStatistics ``` Reports current occupancy and lifetime cache counters. **Returns**: current entries and lifetime counters **Examples** _Reporting whether the cache is doing useful work_ ```kex let stats = resolver.statistics IO.printLine("DNS cache: ${stats.hits} hits, ${stats.misses} misses") ``` #### `close` ```kex close : Void ``` Idempotently closes the resolver. Calls after closing fail with `Closed`; closing again is harmless. ## module `Net.DNS.Name` Validation and IDNA conversion for DNS names. ### `parse` ```kex parse(text: String) -> Result ``` Validates a name and converts Unicode labels to IDNA ASCII. **Parameters** - `text` — a dotted hostname, optionally with a trailing dot **Returns**: the name, or `Parse` **Examples** _Preparing a user-supplied hostname for lookup_ ```kex let name = Name.parse("münich.example").try IO.printLine(name.display) resolver.addresses(name).try ``` ## module `Net.DNS.Resolver` Constructors for long-lived, cache-owning resolvers. ### `system` ```kex system(cache: CacheOptions) -> Result ``` Opens a resolver using system configuration and default cache bounds. Reuse the returned resolver for the lifetime of a service so repeated names benefit from its bounded cache, then close it during shutdown. **Returns**: a resolver handle **Examples** _A service-owned resolver_ ```kex let resolver = Resolver.system.try let upstream = resolver.addresses(Name.parse("api.example.com").try).try resolver.close ``` ### `custom` ```kex custom(options: ResolverOptions) -> Result ``` Opens an isolated resolver with typed nameservers and query bounds. This does not inherit the machine's search domains or nameservers. It is useful for tests, service discovery, and applications with their own DNS policy. **Parameters** - `options` — nameservers, search domains, retry count, per-query timeout, and cache bounds **Returns**: a resolver, or `Parse` **Examples** _Querying a DNS server used by a local integration test_ ```kex let nameserver = Nameserver { address: Net.IP.Address.parse("127.0.0.1").try, port: Net.Port.from(5353).try } Resolver.custom(ResolverOptions { nameservers: [nameserver], timeout: 100.milliseconds }).try ``` ## module `Net.DNS.DNS` Convenience lookup operations for callers that do not need resolver ownership or cache reuse. ### `addresses` ```kex addresses(name: Name) -> Result<[Net.IP.Address], NetError> ``` Resolves a name once with a short-lived system resolver. Prefer this for command-line tools and one-off checks. A server that looks up names repeatedly should own a `Resolver` so it can reuse cached answers. **Parameters** - `name` — the validated hostname **Returns**: addresses or `Resolve` **Examples** _Resolving one host in a short-lived command_ ```kex let addresses = DNS.addresses(Name.parse("example.com").try).try ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: net/http.kex title: Net.HTTP entities: - { kind: module, name: "Net.HTTP" } --- # Net.HTTP ## module `Net.HTTP` Buffered HTTP clients, responses, and a small declaration-ordered server router. Requests never follow redirects or perform generic retries implicitly. ```kex using Net.HTTP let response = HTTP.get("https://example.test/").try response.status.success? # => true ``` For connection reuse and statistics, own a client explicitly: ```kex let client = Client.open.try let response = client.get("https://example.test/").try client.close.try ``` ## record `Headers` An insertion-ordered HTTP field collection. Names compare case-insensitively and duplicate fields are preserved. A list of pairs rather than a `{String: String}` map, and deliberately so. A map cannot hold the same name twice, and `Set-Cookie` needs exactly that: RFC 6265 does not define it as a comma-separated list, so two cookies must travel as two fields and cannot be joined into one. A map interface would read as the obvious one right up to the first response that sets two cookies, then silently keep one — the same class of quiet data loss this module's `Result`-returning builders exist to avoid. Order is kept for the same reason. RFC 9110 makes order insignificant BETWEEN different names but significant between fields sharing a name, and a map has no order to keep. **Fields** - `entries` : [([String](../string.md#make-string), [String](../string.md#make-string))] Implements [`Showable`](../kex.md#trait-showable), [`Inspectable`](../kex.md#trait-inspectable). ### Functions #### `add` ```kex add(name: String, value: String) -> Result ``` Appends a field, keeping existing fields of the same name. Repeats are how `Set-Cookie` works: it is not a comma-separated list, so two cookies must be two fields. For replace-semantics, `remove` first. An invalid name or value is an `Error`, not a silent drop. Rejecting `"a\r\nX: y"` is what stops response splitting, but dropping it quietly left the caller holding a valid `Headers` that simply lacked the field it asked for, and a response with no `Content-Type` invites MIME sniffing. `from` and `parse` already answer with a `Result` for this same input. **Returns**: the extended fields, or `Parse` **Examples** _Two cookies on one response_ ```kex Headers.empty .add("Set-Cookie", "session=abc; HttpOnly").try .add("Set-Cookie", "theme=dark").try ``` _Replacing a field_ ```kex headers.remove("Content-Type").add("Content-Type", "application/json").try ``` #### `remove` ```kex remove(name: String) -> Headers ``` Removes every field matching `name` case-insensitively. **Examples** _Stripping hop-by-hop state before forwarding_ ```kex let forwarded = incoming.remove("Connection") ``` #### `get` ```kex get(name: String) -> String? ``` Returns the first matching field value. **Examples** _Selecting a response decoder_ ```kex let contentType = response.headers.get("Content-Type").or("application/octet-stream") ``` #### `getAll` ```kex getAll(name: String) -> [String] ``` Returns every matching value in insertion order. **Examples** _Preserving every Set-Cookie field_ ```kex let cookies = response.headers.getAll("Set-Cookie") ``` #### `showValue` (from Showable) ```kex showValue : String ``` Renders fields while replacing authorization and cookie values with `***`. #### `inspectValue` (from Inspectable) ```kex inspectValue(colors: Bool) -> String ``` Structural inspection uses the same credential-safe rendering. ### From [`Showable`](../kex.md#trait-showable) - [`to`](../kex.md#showable-to) — ## record `Status` A validated HTTP status code in `100..599`. **Fields** - `code` : [Integer](../number.md#make-integer) ### Functions #### `informational?` ```kex informational? : Bool ``` **Returns**: whether the status is in `100..199` #### `success?` ```kex success? : Bool ``` **Returns**: whether the status is in `200..299` #### `redirect?` ```kex redirect? : Bool ``` **Returns**: whether the status is in `300..399` #### `clientError?` ```kex clientError? : Bool ``` **Returns**: whether the status is in `400..499` #### `serverError?` ```kex serverError? : Bool ``` **Returns**: whether the status is in `500..599` ## record `Response` A typed HTTP response envelope whose body representation is explicit. **Fields** - `status` : [Status](#record-net-http-status) - `headers` : [Headers](#record-net-http-headers) - `body` : B ## record `Request` A typed HTTP request envelope whose body representation is explicit. **Fields** - `method` : [String](../string.md#make-string) - `target` : [URI](../uri.md#record-uri-uri) - `headers` : [Headers](#record-net-http-headers) - `body` : B ## record `RouteContext` Route captures decoded after path segmentation. **Fields** - `parameters` : [Map](../map.md#type-map)<[String](../string.md#make-string), [String](../string.md#make-string)> (optional) ### Functions #### `parameter` ```kex parameter(name: String) -> Result ``` Returns one decoded named or wildcard route capture. **Parameters** - `name` — the capture name declared in the route **Returns**: the capture, or `Parse` when absent **Examples** _Reading +:id+ from a +/users/:id+ route_ ```kex let id = context.parameter("id").try ``` ## record `Context` Per-request server context. **Fields** - `route` : [RouteContext](#record-net-http-routecontext) ## type `Handler` A buffered HTTP route handler. Handling a request is effectful: routes may read files, query a database, send messages, or perform other application IO. ## record `Route` One declared route; routers preserve declaration order. **Fields** - `method` : [String](../string.md#make-string) - `path` : [String](../string.md#make-string) - `handler` : [Handler](#type-net-http-handler) - `maximumBodyBytes` : [Integer](../number.md#make-integer)? (optional) ## record `Router` An immutable, declaration-ordered HTTP router. **Fields** - `routes` : [[Route](#record-net-http-route)] (optional) ### Functions #### `route` ```kex route(method: String, path: String, handler: Handler, maximumBodyBytes: Integer) -> Router ``` Appends a route; earlier matching declarations win. Use this for a method without a convenience function. Paths may include named or wildcard captures, which the handler reads from `RouteContext`. **Examples** _Adding a custom method_ ```kex router.route("PURGE", "/cache/:key", ~purge) ``` #### `get` ```kex get(path: String, handler: Handler, maximumBodyBytes: Integer) -> Router ``` Appends a GET route. GET also supplies automatic HEAD fallback. #### `head` ```kex head(path: String, handler: Handler) -> Router ``` Appends an explicit HEAD route, overriding automatic GET fallback. #### `options` ```kex options(path: String, handler: Handler) -> Router ``` Appends an explicit OPTIONS route, overriding generated OPTIONS. #### `post` ```kex post(path: String, handler: Handler, maximumBodyBytes: Integer) -> Router ``` Appends a POST route. #### `put` ```kex put(path: String, handler: Handler, maximumBodyBytes: Integer) -> Router ``` Appends a PUT route. #### `patch` ```kex patch(path: String, handler: Handler, maximumBodyBytes: Integer) -> Router ``` Appends a PATCH route. #### `delete` ```kex delete(path: String, handler: Handler) -> Router ``` Appends a DELETE route. #### `limited` ```kex limited(method: String, path: String, handler: Handler, maximumBodyBytes: Integer?) -> Router ``` ## record `ShutdownReport` Counts and elapsed time from graceful server shutdown. **Fields** - `completed` : [Integer](../number.md#make-integer) - `failed` : [Integer](../number.md#make-integer) - `forced` : [Integer](../number.md#make-integer) - `elapsedMilliseconds` : [Integer](../number.md#make-integer) ## record `ServerOptions` Bounded HTTP server resources, default graceful-shutdown duration and request body limit. **Fields** - `maximumHandlers` : [Integer](../number.md#make-integer) (optional) - `backlog` : [Integer](../number.md#make-integer) (optional) - `gracefulShutdown` : [Duration](../units.md#record-duration) (optional) - `maximumBodyBytes` : [Integer](../number.md#make-integer) (optional) ## record `RequestOptions` How long one client request may take, and how large an answer it accepts. A request over a size limit is a `Limit` error, and one over a time limit a `Timeout`. **Examples** _A link preview: a short deadline and a small answer_ ```kex RequestOptions { timeout: Duration.seconds(2), maximumBodyBytes: 262144 } ``` **Fields** - `connectTimeout` : [Duration](../units.md#record-duration) (optional) - `timeout` : [Duration](../units.md#record-duration) (optional) - `maximumBodyBytes` : [Integer](../number.md#make-integer) (optional) - `maximumHeaderBytes` : [Integer](../number.md#make-integer) (optional) ## record `PoolOptions` HTTP connection-pool bounds and idle lifetime. **Fields** - `perOrigin` : [Integer](../number.md#make-integer) (optional) - `total` : [Integer](../number.md#make-integer) (optional) - `queuedRequests` : [Integer](../number.md#make-integer) (optional) - `idleExpiryMilliseconds` : [Integer](../number.md#make-integer) (optional) ## record `ClientOptions` Options owned by an explicit HTTP client. **Fields** - `pool` : [PoolOptions](#record-net-http-pooloptions) (optional) - `request` : [RequestOptions](#record-net-http-requestoptions) (optional) ## record `ClientStatistics` Lifetime request/reuse counters plus current pooled connections. **Fields** - `openConnections` : [Integer](../number.md#make-integer) - `requests` : [Integer](../number.md#make-integer) - `reusedConnections` : [Integer](../number.md#make-integer) ## record `ClientCloseReport` Resources released by `Client.close`. **Fields** - `closedConnections` : [Integer](../number.md#make-integer) ## type `Client` The pooled HTTP client. An opaque handle over the connection pool that owns it; `Client.open` makes one and `client.close` releases it. ### Functions #### `request` ```kex request(method: String, url: String, headers: Headers, body: Binary, options: RequestOptions) -> Result, NetError> ``` Sends a buffered request. Redirects and generic retries are not implicit. This keeps policy with the caller: inspect a redirect before following it, and retry only methods and failures your application knows are safe. **Examples** _Sending JSON with an idempotency key_ ```kex let headers = Headers.empty .add("Content-Type", "application/json").try .add("Idempotency-Key", requestId).try client.request("POST", url, headers, JSON.stringify(order).to(Binary).try) ``` #### `get` ```kex get(url: String) -> Result, NetError> ``` Sends a buffered GET request. #### `post` ```kex post(url: String, body: Binary) -> Result, NetError> ``` Sends a buffered binary POST request. #### `put` ```kex put(url: String, body: Binary) -> Result, NetError> ``` Sends a buffered binary PUT request. #### `patch` ```kex patch(url: String, body: Binary) -> Result, NetError> ``` Sends a buffered binary PATCH request. #### `delete` ```kex delete(url: String) -> Result, NetError> ``` Sends a DELETE request with an empty body. #### `head` ```kex head(url: String) -> Result, NetError> ``` Sends a HEAD request; the returned body is empty. #### `options` ```kex options(url: String) -> Result, NetError> ``` Sends an OPTIONS request. #### `statistics` ```kex statistics : ClientStatistics ``` Reports current pool occupancy and lifetime request counters. **Returns**: current pool and lifetime request counters **Examples** _Emitting client-pool diagnostics_ ```kex let stats = client.statistics IO.printLine("HTTP reuse: ${stats.reusedConnections}/${stats.requests}") ``` #### `close` ```kex close : Result ``` Idempotently closes the client and every idle pooled connection. Further requests fail with `Closed`; a second close is harmless. ## module `Net.HTTP.Headers` Construction and parsing of validated HTTP header collections. ### `empty` (constant) ```kex empty : Headers ``` Returns a field collection with no entries. **Examples** _Building request headers immutably_ ```kex Headers.empty .add("Accept", "application/json").try .add("User-Agent", "inventory-sync/1.0").try ``` ### `from` ```kex from(entries: [(String, String)]) -> Result from(entries: Map) -> Result ``` Validates header names and values without folding duplicates. Duplicate fields stay in their original order. Invalid names and values containing line breaks are rejected instead of creating a malformed or injectable HTTP message. **Returns**: validated fields, or `Parse` **Examples** _Forwarding an explicitly selected set of request headers_ ```kex Headers.from([ ("Accept", "application/json"), ("X-Request-ID", requestId) ]).try ``` ### `parse` ```kex parse(text: String) -> Result ``` Parses CRLF- or LF-separated header fields. Use this at a protocol boundary when headers arrive as text. Application code normally builds them with `from`, `add`, and `set`. **Returns**: fields in source order, or `Parse` **Examples** _Parsing headers captured from a diagnostic fixture_ ```kex Headers.parse("Content-Type: text/plain\r\nX-Trace: abc\r\n").try ``` ## module `Net.HTTP.Status` Validation for numeric HTTP status codes. ### `from` ```kex from(code: Integer) -> Result ``` Validates an HTTP status code. **Parameters** - `code` — a status in `100..599` **Returns**: the status, or `Parse` **Examples** _Validating a configurable health-check success code_ ```kex let code = ENV.get("HEALTH_STATUS").flatMap { |text| text.to(Integer) }.or(204) let expected = Status.from(code).try ``` ## module `Net.HTTP.Response` Buffered response constructors for route handlers. ### `binary` ```kex binary(status: Integer, body: Binary, headers: Headers) -> Response ``` Builds a buffered binary response with validated headers. **Examples** _Returning a downloaded file without decoding it as text_ ```kex Response.binary(200, archive, Headers.empty.add("Content-Type", "application/zip").try) ``` ### `text` ```kex text(status: Integer, body: String) -> Response ``` Builds a UTF-8 text response with an explicit text/plain content type. **Examples** _A small health endpoint_ ```kex Response.text(200, "ok") ``` ### `empty` ```kex empty(status: Integer) -> Response ``` Builds a response with an empty body. **Examples** _A successful DELETE endpoint_ ```kex Response.empty(204) ``` ## module `Net.HTTP.Router` The empty starting point for an immutable route declaration chain. ### `build` (constant) ```kex build : Router ``` ## module `Net.HTTP.Server` Starting, observing, and gracefully stopping HTTP servers. ### `start` ```kex start(endpoint: Net.Socket.TCP.Endpoint, router: Router, options: ServerOptions) -> Result ``` Starts a server with conservative defaults and returns immediately. The returned handle owns the listener and active handlers. Use `start` when the process has other work to do; use `serve` for a foreground server whose main job is handling HTTP. **Examples** ```kex let router = Router.build.get("/health", do |request, context| Response.text(200, "ok") end) let endpoint = Net.Socket.TCP.Endpoint.loopback(Net.Port.from(0).try) let server = Server.start(endpoint, router).try Server.stop(server).try ``` ### `serve` ```kex serve(endpoint: Net.Socket.TCP.Endpoint, router: Router, options: ServerOptions) -> Result ``` Starts with defaults and blocks until the server stops. ### `stop` ```kex stop(server: Running, grace: Duration) -> Result ``` Gracefully stops using the duration captured at start. New requests stop being accepted while in-flight handlers get their grace period to finish. The report says how much work completed or was forced down during shutdown. **Examples** _Shutting down from an application lifecycle hook_ ```kex let report = Server.stop(server).try IO.printLine("closed after ${report.completed} requests") ``` ### `join` ```kex join(server: Running) -> Result ``` Waits until the server owner exits. ### `running?` ```kex running?(server: Running) -> Bool ``` **Returns**: whether the server owner is alive ### `localAddress` ```kex localAddress(server: Running) -> Net.Socket.TCP.Endpoint ``` Returns the bound address, including an operating-system-assigned port. **Returns**: the bound address **Examples** _Publishing the actual address of a test server_ ```kex let endpoint = Server.localAddress(server) IO.printLine("test server: http://${endpoint.host}:${endpoint.port.string}") ``` ## type `Running` An opaque asynchronous HTTP server handle. ## module `Net.HTTP.Client` Constructors for explicitly owned, connection-pooling HTTP clients. ### `open` ```kex open : Result open(options: ClientOptions) -> Result ``` Opens an explicit pooled client with conservative defaults. Reuse one client for related requests so keep-alive connections and DNS work can be reused. Close it when the owning service shuts down. **Examples** _Fetching several pages through one connection pool_ ```kex let client = Client.open.try let first = client.get("https://api.example.com/items?page=1").try let second = client.get("https://api.example.com/items?page=2").try client.close.try ``` ## module `Net.HTTP.HTTP` Stateless HTTP convenience calls for scripts and occasional requests. ### `request` ```kex request : String -> String -> Headers -> Binary -> Result, NetError> request : String -> String -> Headers -> Binary -> RequestOptions -> Result, NetError> ``` Sends one stateless buffered request with no redirect or hidden retry. Each call owns a short-lived client. This is convenient for scripts and occasional requests; use `Client` for a service making repeated calls. **Examples** _A one-off authenticated request in a command-line tool_ ```kex let headers = Headers.empty.add("Authorization", "Bearer ${token}").try HTTP.request("GET", url, headers).try ``` ### `get` ```kex get(url: String) -> Result, NetError> ``` Sends one stateless buffered GET. **Examples** ```kex HTTP.get("https://example.test/").try ``` ### `delete` ```kex delete(url: String) -> Result, NetError> ``` Sends one stateless DELETE with an empty body. ### `head` ```kex head(url: String) -> Result, NetError> ``` Sends one stateless HEAD and returns an empty response body. ### `options` ```kex options(url: String) -> Result, NetError> ``` Sends one stateless OPTIONS request. ### `post` ```kex post(url: String, body: Binary) -> Result, NetError> ``` Sends one stateless buffered binary POST. ### `put` ```kex put(url: String, body: Binary) -> Result, NetError> ``` Sends one stateless buffered binary PUT. ### `patch` ```kex patch(url: String, body: Binary) -> Result, NetError> ``` Sends one stateless buffered binary PATCH. --- --- package: prelude version: "0.4.0-rc.3-dev" source: net/ip.kex title: Net.IP entities: - { kind: module, name: "Net.IP" } --- # Net.IP ## module `Net.IP` Validated, canonical IP addresses and CIDR networks. ```kex using Net.IP let address = Address.parse("192.0.2.42").try let network = Network.parse("192.0.2.0/24").try network.contains(address) # => true ``` ## record `Address` A canonical IPv4 or IPv6 address. The stored spelling is normalized, so addresses that arrived in different forms compare and print consistently. Zone identifiers such as `%en0` are properties of socket endpoints and do not belong here. **Fields** - `source` : [String](../string.md#make-string) ### Functions #### `string` ```kex string : String ``` Returns the canonical text form of the address. **Returns**: canonical address text **Examples** _Normalizing an IPv6 address for a log or cache key_ ```kex Address.parse("2001:0db8:0:0::42").try.string # => "2001:db8::42" ``` #### `version` ```kex version : Integer ``` Returns the address family as `4` or `6`. **Returns**: `4` for IPv4 or `6` for IPv6 **Examples** _Selecting a family-specific socket policy_ ```kex let family = address.version == 6 then :ipv6 else :ipv4 ``` #### `loopback?` ```kex loopback? : Bool ``` Returns `true` for an address that routes back to this host. This covers the IPv4 `127.0.0.0/8` block as well as IPv6 `::1`; it is not limited to the familiar `127.0.0.1` spelling. **Returns**: whether this is a loopback address **Examples** _Refusing to expose a development service beyond this machine_ ```kex die("development server must use loopback") if !address.loopback? ``` #### `private?` ```kex private? : Bool ``` Returns `true` for an address reserved for private networks. Use this as one signal in a network policy, not as proof that a peer is trusted: private addresses can still belong to another machine. **Returns**: whether this is a private-use address **Examples** _Rejecting a public target in an internal-only configuration_ ```kex Error("target must be private") if !target.private? ``` #### `unspecified?` ```kex unspecified? : Bool ``` Returns `true` for the all-zero address: `0.0.0.0` or `::`. On a listening endpoint this usually means "all local interfaces". It is not a usable remote destination. **Returns**: whether every address bit is zero **Examples** _Warning before binding a service on every interface_ ```kex IO.warn("service will be publicly reachable") if bind.unspecified? ``` #### `multicast?` ```kex multicast? : Bool ``` Returns `true` when the address names a multicast group. **Returns**: whether this is a multicast address **Examples** _Choosing multicast-specific socket setup_ ```kex let mode = destination.multicast? then :group else :unicast ``` ## record `Network` A canonical CIDR network with host bits cleared. Parsing `192.0.2.9/24` therefore produces `192.0.2.0/24`. This makes a `Network` suitable for access-control rules and routing tables: its identity is the range, not whichever host address happened to describe it. **Fields** - `source` : [String](../string.md#make-string) ### Functions #### `string` ```kex string : String ``` Returns canonical CIDR text, including the prefix length. **Returns**: canonical CIDR text **Examples** ```kex Network.parse("192.0.2.99/24").try.string # => "192.0.2.0/24" ``` #### `contains` ```kex contains(address: Address) -> Bool ``` Returns `true` when `address` falls within this network. An address from the other family is simply outside the network; callers do not need to compare `version` first. **Parameters** - `address` — the address to test **Returns**: whether `address` belongs to this network **Examples** _Checking an application allowlist_ ```kex let office = Network.parse("198.51.100.0/24").try let allowed? = office.contains(requestAddress) ``` #### `prefix` ```kex prefix : Integer ``` Returns the number of fixed leading address bits. **Returns**: the prefix length **Examples** ```kex Network.parse("10.0.0.0/8").try.prefix # => 8 ``` #### `first` ```kex first : Address ``` Returns the first address in the range: the address with all host bits cleared. **Returns**: the first address in the range **Examples** ```kex Network.parse("192.0.2.9/24").try.first.string # => "192.0.2.0" ``` #### `last` ```kex last : Address ``` Returns the last address in the range: the address with all host bits set. This is the IPv4 broadcast-shaped endpoint of the mathematical range; the API does not decide whether a protocol permits assigning it to a host. **Returns**: the last address in the range **Examples** ```kex Network.parse("192.0.2.9/24").try.last.string # => "192.0.2.255" ``` ## module `Net.IP.Address` Strict parsing and canonicalization of individual IP addresses. ### `parse` ```kex parse(text: String) -> Result ``` Parses IPv4 or IPv6 text and canonicalizes its spelling. **Parameters** - `text` — an IPv4 dotted quad or IPv6 address **Returns**: the address, or `Parse` **Examples** _Canonicalize an IPv6 address_ ```kex Address.parse("2001:db8::42").try.string # => "2001:db8::42" ``` ## module `Net.IP.Network` Strict parsing and canonicalization of CIDR networks. ### `parse` ```kex parse(text: String) -> Result ``` Parses a CIDR and clears host bits. **Parameters** - `text` — an address followed by a prefix length **Returns**: the network, or `Parse` **Examples** _Normalize a host address to its network_ ```kex Network.parse("192.0.2.9/24").try.string # => "192.0.2.0/24" ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: net/socket.kex title: Net.Socket entities: - { kind: module, name: "Net.Socket" } --- # Net.Socket ## module `Net.Socket` Process-owned TCP byte streams and listeners. All blocking operations return typed `NetError` values; close operations are idempotent. ```kex using Net using Net.Socket let listener = TCP.listen(TCP.Endpoint.loopback(Port.from(0).try)).try let address = listener.localAddress.try let client = TCP.connect(address).try client.sendAll("ping".to(Binary).try).try listener.close ``` ## module `Net.Socket.TCP` ### `connect` ```kex connect(endpoint: Endpoint) -> Result connect(endpoint: Endpoint, options: ConnectOptions) -> Result ``` Connects to a TCP endpoint with the backend's bounded connect deadline. **Returns**: a stream or `Connect`/`Timeout` **Examples** _Connecting to a line-oriented local service_ ```kex let endpoint = TCP.Endpoint.host("127.0.0.1", Port.from(9000).try) let connection = TCP.connect(endpoint).try ``` ### `listen` ```kex listen(endpoint: Endpoint) -> Result listen(endpoint: Endpoint, options: ListenOptions) -> Result ``` Binds and starts listening. Port zero selects an ephemeral local port. **Returns**: a listener or `Connect` **Examples** _Giving each integration test its own free port_ ```kex let listener = TCP.listen(TCP.Endpoint.loopback(Port.from(0).try)).try let port = listener.localAddress.try.port ``` ### `sendAll` ```kex sendAll(connection: TCPConnection, data: Binary) -> Result ``` Sends every byte, retrying partial operating-system writes internally. On failure, `NetError.progress` records how many bytes were accepted before the error. Do not blindly retry the whole payload when progress is present. **Examples** _Sending one newline-delimited request_ ```kex connection.sendAll("status\n".to(Binary).try).try ``` ### `receiveChunk` ```kex receiveChunk(connection: TCPConnection, limit: Integer, timeout: Duration) -> Result ``` Receives up to `limit` bytes; EOF is reported as `Closed`. A successful result is one available chunk, not necessarily a complete application message. Use `receiveExactly`, `receiveUntil`, or `receiveLine` when the protocol supplies a boundary. **Examples** _Reading up to 16 KiB from a streaming response_ ```kex let chunk = connection.receiveChunk(16 * 1024).try ``` ### `receiveExactly` ```kex receiveExactly(connection: TCPConnection, count: Integer, timeout: Duration) -> Result ``` Receives exactly `count` bytes or returns a typed EOF/timeout failure. Useful after a protocol header has declared the payload length. **Examples** _Reading a four-byte frame header_ ```kex let header = connection.receiveExactly(4).try ``` ### `receiveUntil` ```kex receiveUntil(connection: TCPConnection, delimiter: Binary, limit: Integer, timeout: Duration) -> Result ``` Receives through the first `delimiter` without exceeding `limit` bytes. The delimiter is included in the returned bytes. An empty delimiter is a `Parse` error; reaching the bound first is a `Limit` error. **Examples** _Read through the end of an HTTP header section_ ```kex connection.receiveUntil("\r\n\r\n".to(Binary).try, 65536).try ``` ### `receiveLine` ```kex receiveLine(connection: TCPConnection, limit: Integer, timeout: Duration) -> Result ``` Receives through a newline without exceeding `limit` bytes. The newline remains in the returned binary. Decode and trim only after a complete bounded line has been received. **Examples** _Reading a response line with a defensive 8 KiB limit_ ```kex let line = connection.receiveLine(8192).try.to(String).try.trim ``` ### `shutdownWrite` ```kex shutdownWrite(connection: TCPConnection) -> Result ``` Half-closes the write side while leaving reads available. ### `accept` ```kex accept(listener: TCPListener, timeout: Duration) -> Result ``` Waits for and returns the next connection accepted by the listener. A timeout applies to this wait only; it does not become a read timeout on the returned connection. **Examples** _Accepting one client in a small test server_ ```kex let client = listener.accept(5.seconds).try ``` ### `close` ```kex close(connection: TCPConnection) -> Void ``` Idempotently closes a connected stream. ### `closed?` ```kex closed?(connection: TCPConnection) -> Bool ``` **Returns**: whether the connected stream owner has stopped ### `localAddress` ```kex localAddress(connection: TCPConnection) -> Result ``` Returns the bound local endpoint, including an ephemeral assigned port. ### `peerAddress` ```kex peerAddress(connection: TCPConnection) -> Result ``` Returns the remote endpoint of a connected stream. **Examples** _Recording the peer in an access log_ ```kex let peer = connection.peerAddress.try IO.printLine("accepted ${peer.host}:${peer.port.string}") ``` ## type `Plain` Marker for an unencrypted stream. ## type `TCPConnection` An opaque connected TCP stream. ## type `TCPListener` An opaque TCP listening socket. ## record `Endpoint` A host name or numeric address paired with a validated port. **Fields** - `host` : [String](../string.md#make-string) - `port` : Net.Port ## record `ConnectOptions` Connection deadlines and operating-system socket policy. Buffer values of zero leave sizing to the operating system. `noDelay?` disables Nagle's algorithm, which is usually right for request/response traffic; bulk-transfer protocols may prefer fewer, larger packets. **Fields** - `connectTimeout` : [Duration](../units.md#record-duration) (optional) - `noDelay?` : [Bool](../truthyable.md#make-bool) (optional) - `keepAlive?` : [Bool](../truthyable.md#make-bool) (optional) - `sendBuffer` : [Integer](../number.md#make-integer) (optional) - `receiveBuffer` : [Integer](../number.md#make-integer) (optional) ## record `ListenOptions` Listener queue and policy inherited by accepted connections. `backlog` bounds connections waiting for `accept`. Buffer values of zero keep the platform defaults rather than requesting a particular byte size. **Fields** - `backlog` : [Integer](../number.md#make-integer) (optional) - `reuseAddress?` : [Bool](../truthyable.md#make-bool) (optional) - `noDelay?` : [Bool](../truthyable.md#make-bool) (optional) - `keepAlive?` : [Bool](../truthyable.md#make-bool) (optional) - `sendBuffer` : [Integer](../number.md#make-integer) (optional) - `receiveBuffer` : [Integer](../number.md#make-integer) (optional) ## module `Net.Socket.TCP.Endpoint` ### `host` ```kex host(name: String, port: Net.Port) -> Endpoint ``` Pairs a hostname or numeric address with a port. Resolution happens when connecting, so this preserves `name` exactly as supplied rather than validating it as an `IP.Address`. **Returns**: the remote or listening endpoint **Examples** _Connecting by hostname_ ```kex TCP.Endpoint.host("cache.internal", Port.from(6379).try) ``` ### `any` ```kex any(port: Net.Port) -> Endpoint ``` Builds an IPv4 wildcard endpoint for listening on every local interface. Be deliberate with this in development: unlike `loopback`, it may expose the service to other machines on the network. **Returns**: an IPv4 wildcard listening endpoint **Examples** _Exposing a production service on port 8080_ ```kex TCP.Endpoint.any(Port.from(8080).try) ``` ### `loopback` ```kex loopback(port: Net.Port) -> Endpoint ``` Builds an IPv4 loopback endpoint reachable only from this machine. Port zero lets the operating system choose a free port, which is useful for tests; ask `localAddress` which port was assigned after listening. **Returns**: an IPv4 loopback endpoint **Examples** _Starting an isolated test listener_ ```kex TCP.Endpoint.loopback(Port.from(0).try) ``` ## module `Net.Socket.UDP` ### `bind` ```kex bind(endpoint: Endpoint) -> Result bind(endpoint: Endpoint, options: BindOptions) -> Result ``` Binds a datagram socket. Port zero selects an ephemeral local port. **Examples** _A local UDP echo test_ ```kex let socket = UDP.bind(UDP.Endpoint.loopback(Port.from(0).try)).try let endpoint = socket.localAddress.try ``` ### `sendTo` ```kex sendTo(socket: Socket, endpoint: Endpoint, data: Binary) -> Result ``` Sends one complete datagram and returns its byte count. Datagram boundaries are preserved: one `sendTo` corresponds to one `receiveFrom`, unless the packet is lost by the network. **Examples** _Sending a StatsD-style counter_ ```kex socket.sendTo(collector, "orders:1|c".to(Binary).try).try ``` ### `receiveFrom` ```kex receiveFrom(socket: Socket, limit: Integer) -> Result ``` Receives one datagram no larger than `limit` bytes. Oversized packets fail with `Limit` instead of being silently truncated, so a caller never mistakes a prefix for a complete message. **Examples** _Receiving and replying to one packet_ ```kex let packet = socket.receiveFrom(4096).try socket.sendTo(packet.source, packet.data).try ``` ### `close` ```kex close(socket: Socket) -> Void ``` Idempotently closes the socket. ### `closed?` ```kex closed?(socket: Socket) -> Bool ``` **Returns**: whether the socket owner has stopped ### `localAddress` ```kex localAddress(socket: Socket) -> Result ``` Returns the bound endpoint, including an ephemeral assigned port. ### `joinMulticast` ```kex joinMulticast(socket: Socket, group: Net.IP.Address, interface: Net.IP.Address) -> Result ``` Joins an IPv4 multicast group on the selected local interface. The group must be multicast and the interface must be an IPv4 address. ### `leaveMulticast` ```kex leaveMulticast(socket: Socket, group: Net.IP.Address, interface: Net.IP.Address) -> Result ``` Leaves a membership previously joined with the same group and interface. ## type `Socket` Connectionless datagrams. A receive limit rejects an oversized datagram instead of returning a silently truncated payload. ```kex let socket = UDP.bind(UDP.Endpoint.loopback(Port.from(0).try)).try let address = socket.localAddress.try socket.sendTo(address, "hello".to(Binary).try).try let packet = socket.receiveFrom(1024).try socket.close ``` An opaque bound datagram socket. ## record `Endpoint` A datagram address paired with a validated port. **Fields** - `host` : [String](../string.md#make-string) - `port` : Net.Port ## record `Datagram` A received datagram and its source endpoint. Reply to `source` rather than the socket's local address: UDP has no connection that remembers which peer sent the packet. **Fields** - `source` : Endpoint - `data` : [Binary](../binary.md#type-binary) ## record `BindOptions` Curated socket policy. Broadcast is opt-in. The receive timeout applies to each `receiveFrom` call; multicast TTL is bounded to the IP hop-limit range. **Fields** - `broadcast?` : [Bool](../truthyable.md#make-bool) (optional) - `multicastTtl` : [Integer](../number.md#make-integer) (optional) - `multicastLoopback?` : [Bool](../truthyable.md#make-bool) (optional) - `receiveTimeout` : [Duration](../units.md#record-duration) (optional) ## module `Net.Socket.UDP.Endpoint` ### `host` ```kex host(name: String, port: Net.Port) -> Endpoint ``` Pairs a hostname or numeric address with a UDP port. **Returns**: an endpoint using `name` exactly as supplied **Examples** _Addressing a local metrics collector_ ```kex UDP.Endpoint.host("metrics.internal", Port.from(8125).try) ``` ### `any` ```kex any(port: Net.Port) -> Endpoint ``` Builds an IPv4 wildcard endpoint for receiving on every local interface. **Returns**: an IPv4 wildcard endpoint **Examples** _Receiving discovery packets on port 9999_ ```kex UDP.Endpoint.any(Port.from(9999).try) ``` ### `loopback` ```kex loopback(port: Net.Port) -> Endpoint ``` Builds an IPv4 loopback endpoint reachable only from this machine. **Returns**: an IPv4 loopback endpoint **Examples** _Allocating a free UDP port for a test_ ```kex UDP.Endpoint.loopback(Port.from(0).try) ``` ## module `Net.Socket.Unix` ### `connect` ```kex connect(address: Address) -> Result connect(address: Address, options: ConnectOptions) -> Result ``` Connects to a filesystem-domain listener. Unix sockets avoid opening a network port and are a good fit for two processes on the same machine, such as a CLI and its background daemon. **Examples** ```kex let daemon = Unix.connect(Unix.Address.path("/tmp/my-app.sock").try).try ``` ### `listen` ```kex listen(address: Address) -> Result listen(address: Address, options: ListenOptions) -> Result ``` Binds a new path; an existing filesystem entry is never removed implicitly. This conservative default protects regular files and also avoids taking over a socket that may still belong to a running service. ### `sendAll` ```kex sendAll(connection: UnixConnection, data: Binary) -> Result ``` Sends every byte and returns the count. ### `receiveChunk` ```kex receiveChunk(connection: UnixConnection, limit: Integer) -> Result ``` Receives one bounded chunk; EOF is `Closed`. ### `receiveExactly` ```kex receiveExactly(connection: UnixConnection, count: Integer) -> Result ``` Receives exactly `count` bytes or returns a typed EOF/timeout failure. ### `receiveUntil` ```kex receiveUntil(connection: UnixConnection, delimiter: Binary, limit: Integer) -> Result ``` Receives through `delimiter`, including it, within an explicit bound. **Examples** _Read through the next newline_ ```kex connection.receiveUntil("\n".to(Binary).try, 4096).try ``` ### `receiveLine` ```kex receiveLine(connection: UnixConnection, limit: Integer) -> Result ``` Receives through a newline without exceeding `limit` bytes. ### `shutdownWrite` ```kex shutdownWrite(connection: UnixConnection) -> Result ``` Half-closes the write side while leaving reads available. ### `accept` ```kex accept(listener: UnixListener) -> Result ``` Waits for and returns the next stream connection. ### `close` ```kex close(connection: UnixConnection) -> Void ``` Idempotently closes a stream. ### `closed?` ```kex closed?(connection: UnixConnection) -> Bool ``` **Returns**: whether the stream owner has stopped ## type `UnixConnection` Filesystem-domain streams for local IPC. The listener owns and removes only the socket path it successfully created. ```kex let address = Unix.Address.path("/tmp/my-service.sock").try let listener = Unix.listen(address).try let client = Unix.connect(address).try listener.close ``` An opaque connected filesystem-domain byte stream. ## type `UnixListener` An opaque filesystem-domain stream listener. ## record `Address` A validated absolute filesystem socket path. **Fields** - `path` : [String](../string.md#make-string) ### Defined in other modules - [Net.IP](../net/ip.md#make-address): [`string`](../net/ip.md#address-string), [`version`](../net/ip.md#address-version), [`loopback?`](../net/ip.md#address-loopback?), [`private?`](../net/ip.md#address-private?), [`unspecified?`](../net/ip.md#address-unspecified?), [`multicast?`](../net/ip.md#address-multicast?) ## record `ConnectOptions` Connection and read deadlines for a local IPC client. **Fields** - `connectTimeout` : [Duration](../units.md#record-duration) (optional) - `receiveTimeout` : [Duration](../units.md#record-duration) (optional) ## record `ListenOptions` Listener queue, stale-socket policy, and per-operation deadlines. `removeStale?` removes only a filesystem socket, never a regular file or directory that happens to occupy the requested path. **Fields** - `backlog` : [Integer](../number.md#make-integer) (optional) - `removeStale?` : [Bool](../truthyable.md#make-bool) (optional) - `acceptTimeout` : [Duration](../units.md#record-duration) (optional) - `receiveTimeout` : [Duration](../units.md#record-duration) (optional) ## module `Net.Socket.Unix.Address` ### `path` ```kex path(value: String) -> Result ``` Validates a nonempty absolute Unix-domain socket path. Relative paths are rejected so ownership and cleanup always refer to one unambiguous filesystem entry. **Returns**: the address, or `Parse` **Examples** _Addressing a per-user background service_ ```kex Unix.Address.path("/tmp/my-app.sock").try ``` ## module `Net.Socket.TLS` ### `connect` ```kex connect(endpoint: Net.Socket.TCP.Endpoint, config: ClientConfig) -> Result ``` Opens a direct TLS connection with a bounded handshake deadline. `serverName` drives both certificate hostname verification and SNI. Pass the DNS name from the URL, not an address it happened to resolve to. **Returns**: a TLS stream or typed failure **Examples** _Opening a verified connection to an HTTPS origin_ ```kex let endpoint = TCP.Endpoint.host("example.com", Port.from(443).try) let connection = TLS.connect(endpoint, TLS.ClientConfig { serverName: "example.com", alpn: ["http/1.1"] }).try ``` ### `sendAll` ```kex sendAll(connection: TLSConnection, data: Binary) -> Result ``` Sends every plaintext byte through the TLS stream. ### `receiveChunk` ```kex receiveChunk(connection: TLSConnection, limit: Integer) -> Result ``` Receives one decrypted chunk no larger than `limit`. ### `close` ```kex close(connection: TLSConnection) -> Void ``` Idempotently closes the TLS stream. ### `closed?` ```kex closed?(connection: TLSConnection) -> Bool ``` **Returns**: whether the TLS connection owner has stopped ## type `TLSConnection` TLS client streams. Certificate and hostname verification are enabled by default; disabling verification must be an explicit configuration choice. ```kex let endpoint = TCP.Endpoint.host("example.test", Port.from(443).try) let tls = TLS.connect(endpoint, TLS.ClientConfig { serverName: "example.test" }).try tls.close ``` An opaque verified or explicitly unverified TLS byte stream. ## record `ClientConfig` Client handshake policy. TLS 1.2/1.3 are enabled; verification defaults on. **Fields** - `serverName` : [String](../string.md#make-string) - `verify?` : [Bool](../truthyable.md#make-bool) (optional) - `alpn` : [[String](../string.md#make-string)] (optional) --- --- package: prelude version: "0.4.0-rc.3-dev" source: net/http/websocket.kex title: Net.HTTP.WebSocket entities: - { kind: module, name: "Net.HTTP.WebSocket" } --- # Net.HTTP.WebSocket ## module `Net.HTTP.WebSocket` High-level RFC 6455 client messages. The runtime handles fragmentation and ping/pong frames; reconnect and heartbeat policies remain application-owned. A `Connection` delivers complete messages rather than wire frames. Your code never has to assemble fragments or answer a protocol ping, but it does decide what a dropped connection means: reconnecting may require resubscribing or replaying an application cursor, so the library cannot do that safely for you. ```kex using Net.HTTP.WebSocket let socket = WebSocket.connect("wss://example.test/events").try socket.send(Text("hello")).try let message = socket.receiveMessage.try socket.close ``` ## type `Message` A complete high-level WebSocket message. Fragmentation and ping/pong control frames are handled by the connection runtime. `CloseMessage` carries the peer's status code and reason. Treat it as the end of the message stream even when the code describes a normal shutdown. **Variants** - `Text(String)` - `BinaryMessage(Binary)` - `CloseMessage(Integer, String)` ## record `ClientOptions` Client handshake policy and the maximum reassembled message size. Subprotocols are offered in preference order. The byte limit applies after fragments are reassembled, preventing a peer from bypassing the bound with many individually small frames. **Fields** - `subprotocols` : [[String](../../string.md#make-string)] (optional) - `maximumMessageBytes` : [Integer](../../number.md#make-integer) (optional) ## record `Session` Negotiated handshake information. The subprotocol is `None` when the server selected none. **Fields** - `subprotocol` : [String](../../string.md#make-string)? ## type `Connection` An opaque RFC 6455 connection, client- or server-side. It does not reconnect automatically. ### Functions #### `send` ```kex send(message: Message) -> Result ``` Sends one masked text, binary, or close message. **Returns**: success or `Protocol`/`Limit`/`Closed` **Examples** _Subscribing after connecting_ ```kex connection.send(Text(JSON.stringify({ action: "subscribe", topic: topic }))).try ``` #### `receiveMessage` ```kex receiveMessage(timeout: Duration?) -> Result ``` `receive` is a Kex process keyword, so the public function spells out the operation while preserving the plan's high-level message semantics. Reassembles fragments, validates UTF-8, and automatically answers pings. A `CloseMessage` is returned once so the application can inspect the peer's reason. Subsequent reads fail with `Closed`. **Returns**: the next data or close message **Examples** _Processing messages until the server closes the session_ ```kex loop do match connection.receiveMessage.try do Text(text) => handleEvent(text) BinaryMessage(data) => saveSnapshot(data) CloseMessage(code, reason) => break end end ``` #### `session` ```kex session : Session ``` Returns handshake details negotiated with the server. **Returns**: the selected subprotocol, if any **Examples** _Verifying which compatible event format was selected_ ```kex let protocol = connection.session.subprotocol.or("default") ``` #### `close` ```kex close : Void ``` Sends a normal close frame and idempotently releases the transport. Use an explicit `CloseMessage` with `send` first when the peer needs an application-specific code or reason. #### `closed?` ```kex closed? : Bool ``` **Returns**: whether the connection owner has stopped ## record `Handshake` Subprotocols offered by an incoming upgrade request, in the order the peer listed them. **Fields** - `subprotocols` : [[String](../../string.md#make-string)] (optional) ## type `Upgrade` A server's decision after inspecting a `Handshake`. `Accept` takes over the connection once the 101 response is sent: `handler` runs with the negotiated server `Connection`, and its return value is discarded. `headers` are added to the 101 response; a name that manages the handshake itself (`Upgrade`, `Connection`, `Sec-WebSocket-Accept`, `Sec-WebSocket-Protocol`) is dropped rather than overridden. `subprotocol` must be one `handshake.subprotocols` actually offered, or `None`. `Reject` answers with an ordinary buffered response instead, leaving the connection as plain HTTP — a client requesting an unsupported subprotocol might get `Response.text(426, "chat.v2 required")`, for instance. **Variants** - `Accept((Connection) -> Void, Headers, String?)` - `Reject(Response)` ## module `Net.HTTP.WebSocket.WebSocket` Constructors for high-level WebSocket connections, client- and server-side. ### `connect` ```kex connect(url: String, options: ClientOptions) -> Result ``` Opens a `ws:` or verified `wss:` connection with default options. **Parameters** - `url` — an absolute WebSocket URL **Returns**: a connection or typed handshake error **Examples** _Following a live event feed_ ```kex let socket = WebSocket.connect("wss://events.example.com/orders").try match socket.receiveMessage.try do Text(json) => IO.printLine(json) _ => IO.warn("unexpected non-text event") end ``` ### `upgrade` ```kex upgrade(request: Request, decide: (Handshake -> Upgrade)) -> Response ``` Decides whether to accept an incoming `Net.HTTP.Server` upgrade request. Call from a route handler and return the result directly — it types as an ordinary `Response`, and `Net.HTTP.Server` recognizes what it actually is: a request that isn't a syntactically valid WebSocket handshake at all (wrong method, missing `Sec-WebSocket-Key`, unsupported `Sec-WebSocket-Version`) is answered automatically without calling `decide`; a valid one reaches `decide` for an application decision. **Parameters** - `request` — the route handler's own request - `decide` — the accept/reject decision **Returns**: the handler's response — an upgrade in disguise on `Accept`, sent as given on `Reject` **Examples** _An authenticated, subprotocol-gated chat route_ ```kex foul socketRoute(request: Request, context: Context) -> Response = WebSocket.upgrade(request) do |handshake| if handshake.subprotocols.contains?("chat.v2") let handler : Connection -> Void = { |socket| serveChat(socket) } Accept(handler, Headers.empty, Just("chat.v2")) else Reject(Response.text(426, "chat.v2 required")) end end let router = Router.build.get("/socket", ~socketRoute) ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: units/data.kex title: Units.Data entities: - { kind: module, name: "Units.Data" } --- # Units.Data ## module `Units.Data` Data sizes: bytes, kilobytes, and their binary counterparts. Opt-in: nothing here is in scope until `using Units.Data`. ```kex using Units.Data main do IO.printLine(1500.megabyte.to(String)) # prints: 1500.0 MB IO.printLine(1500000000.byte.to(String, in: GB).or("")) # prints: 1.5 GB end ``` Both families are here and they are not the same: `KB` is 1000 bytes, `KiB` is 1024. Values built from either convert freely, because both are counted in bytes underneath, so `1.gibibyte.convertTo(MiB)` answers 1024 MiB. Numeric constructors use singular names, as in `Units.SI`: `5.megabyte` and `2.mebibyte`. `byteSize` is an alias for `byte`. File and payload APIs return integer byte counts. Wrap those counts with `byte` to convert or display them as measurements: ```kex FS.File.size("report.pdf").map { |n| n.byte.showValue } Binary.fromBytes([104, 105]).length.byte.to(String) # => Just("2.0 B") "é".bytes.count.byte.to(String) # => Just("2.0 B") ``` To display a size in another unit, decimal or binary, name the unit: ```kex 1048576.byte.to(String, in: MiB) # => Just("1.0 MiB") 4312.byte.to(String, in: KB, format: Number.format`#.#`) # => Just("4.3 KB") 1048576.byte.convertTo(MiB).map { |m| m.showValue } # => Ok("1.0 MiB") ``` Compare an integer byte count directly with a data-size threshold: ```kex 30000001 > 30.megabyte # => true 1048576 <= 1.mebibyte # => true ``` `<`, `>`, `<=`, and `>=` work with the count on either side. A time or SI measure is not a byte threshold and raises an error. Every value is a `Measure` from the prelude, so its arithmetic and `to(String)` apply unchanged. ### `size` ```kex size(value: Number, unit: DataUnit) -> Measure ``` `value` of the given data unit, as a `Measure`. The general constructor the named ones below are written on. Reach for `megabyte`, `kibibyte` and friends when the unit is known at the call site. **Parameters** - `value` — the quantity - `unit` — the unit it is counted in **Returns**: the size **Examples** ```kex size(4096, B).to(String) # => Just("4096.0 B") size(1.5, MB).to(String) # => Just("1.5 MB") ``` ### `convertTo` ```kex convertTo(measure: Measure, unit: DataUnit) -> Result ``` Converts a size to another data unit. Decimal and binary units convert freely, because both are counted in bytes underneath. A measure of some other dimension is an `Error`. **Parameters** - `measure` — the size to convert - `unit` — the unit to convert to **Returns**: the converted size, or why it could not be **Examples** ```kex 1.gibibyte.convertTo(MiB).map { |m| m.showValue } # => Ok("1024.0 MiB") 2.gigabyte.convertTo(MB).map { |m| m.showValue } # => Ok("2000.0 MB") ``` ### `byteSize` ```kex byteSize(value: Number) -> Measure ``` `value` bytes. Named `byteSize` rather than `bytes`, which `String` already uses for its UTF-8 encoding. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 4096.byteSize.to(String) # => Just("4096.0 B") ``` _Reporting a file's size_ ```kex FS.File.size("report.pdf").map { |n| n.byte.showValue } ``` ### `byte` ```kex byte(value: Number) -> Measure ``` `value` bytes, as a `Measure`. Use this with byte counts from `FS.File.size` or `Binary.length`. **Parameters** - `value` — the byte count **Returns**: the size **Examples** _A binary payload's size_ ```kex let payload = Binary.fromBytes([104, 105]) payload.length.byte.to(String) # => Just("2.0 B") ``` ### `kilobyte` ```kex kilobyte(value: Number) -> Measure ``` `value` kilobytes, 1000 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 5.kilobyte.to(String) # => Just("5.0 KB") ``` ### `megabyte` ```kex megabyte(value: Number) -> Measure ``` `value` megabytes, 1000000 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 1500.megabyte.to(String) # => Just("1500.0 MB") ``` ### `gigabyte` ```kex gigabyte(value: Number) -> Measure ``` `value` gigabytes, 10^9 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 2.gigabyte.convertTo(MB).map { |m| m.showValue } # => Ok("2000.0 MB") ``` ### `terabyte` ```kex terabyte(value: Number) -> Measure ``` `value` terabytes, 10^12 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 2.terabyte.to(String) # => Just("2.0 TB") ``` ### `kibibyte` ```kex kibibyte(value: Number) -> Measure ``` `value` kibibytes, 1024 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 1024.kibibyte.to(String) # => Just("1024.0 KiB") ``` ### `mebibyte` ```kex mebibyte(value: Number) -> Measure ``` `value` mebibytes, 1024^2 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 3.mebibyte.to(String) # => Just("3.0 MiB") ``` ### `gibibyte` ```kex gibibyte(value: Number) -> Measure ``` `value` gibibytes, 1024^3 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 1.gibibyte.convertTo(MiB).map { |m| m.showValue } # => Ok("1024.0 MiB") ``` ### `tebibyte` ```kex tebibyte(value: Number) -> Measure ``` `value` tebibytes, 1024^4 bytes each. **Parameters** - `value` — the quantity **Returns**: the size **Examples** ```kex 1.tebibyte.to(String) # => Just("1.0 TiB") ``` ## type `DataUnit` The data units this module names: decimal (`KB`, `MB`, `GB`, `TB`) and binary (`KiB`, `MiB`, `GiB`, `TiB`), plus the plain byte `B`. ```kex size(1, KB).canonical # => 1000.0 bytes size(1, KiB).canonical # => 1024.0 bytes ``` **Variants** - `B` - `KB` - `MB` - `GB` - `TB` - `KiB` - `MiB` - `GiB` - `TiB` Implements [`Unit`](../units.md#trait-unit). ### Functions #### `factor` (from Unit) ```kex factor(_) ``` #### `kind` (from Unit) #### `symbol` (from Unit) ```kex symbol(_) ``` ## extends `UnitDefinition` More functions for [`UnitDefinition`](../units.md#record-unitdefinition), added by this module. Implements [`Unit`](../units.md#trait-unit). ### `factor` (from Unit) ### `kind` (from Unit) ### `symbol` (from Unit) ## extends `Measure` More functions for [`Measure`](../units.md#record-measure), added by this module. Displaying a size in another data unit: the prelude's `to(String, in:)`, for this module's units. ### `factor` ```kex factor : Float ``` ### `kind` ```kex kind : Atom ``` ### `symbol` ```kex symbol : String ``` ### `to` ```kex to(String: Type, in: DataUnit, format: NumberFormat) -> String? ``` Renders a size in another data unit, decimal or binary. A measure that is not a data size answers `None`. **Parameters** - `String` — the target type - `in` — the unit to display in **Returns**: the rendered size, or `None` **Examples** ```kex 1500000000.byte.to(String, in: GB) # => Just("1.5 GB") 1048576.byte.to(String, in: MiB) # => Just("1.0 MiB") 1.second.to(String, in: MB) # => None ``` ### `<` ```kex <(other: Integer) -> Bool ``` ### `>` ```kex >(other: Integer) -> Bool ``` ### `<=` ```kex <=(other: Integer) -> Bool ``` ### `>=` ```kex >=(other: Integer) -> Bool ``` ## extends `Integer` More functions for [`Integer`](../number.md#make-integer), added by this module. Compares an integer byte count with a data-size threshold. File and binary sizes stay integers. Import this module to use data units directly in comparisons; only data measures are accepted as thresholds. **Examples** _Rejecting an oversized file_ ```kex FS.File.size("report.pdf").map { |count| count > 30.megabyte } ``` _Checking a payload_ ```kex Binary.fromBytes([104, 105]).length < 1.kibibyte # => true ``` ### `<` ```kex <(other: Measure) -> Bool ``` ### `>` ```kex >(other: Measure) -> Bool ``` ### `<=` ```kex <=(other: Measure) -> Bool ``` ### `>=` ```kex >=(other: Measure) -> Bool ``` --- --- package: prelude version: "0.4.0-rc.3-dev" source: units/dynamic.kex title: Units.Dynamic entities: - { kind: module, name: "Units.Dynamic" } --- # Units.Dynamic ## module `Units.Dynamic` Runtime dimensional quantities. Unit dimensions use nominal Type identities, and all arithmetic composes the same normalized exponent maps. ### `unit` ```kex unit(scale: Float, dimension: Dimension, notation: String) -> Result ``` Define a positive, finite scale to the dimension's canonical unit. ### `measure` ```kex measure(value: Float, unit: DynamicUnit) -> DynamicMeasure ``` Build a quantity in its preferred display unit. ## record `DynamicUnit` **Fields** - `scale` : [Float](../number.md#make-float) - `dimension` : [Dimension](../dimensions.md#record-dimensions-dimension) - `notation` : [String](../string.md#make-string) ### Functions #### `*` ```kex *(other: DynamicUnit) -> Result ``` Preserve grouping in display expressions as well as dimensions and scale. #### `/` ```kex /(other: DynamicUnit) -> Result ``` #### `pow` ```kex pow(exponent: Integer) -> Result ``` #### `named` ```kex named(notation: String) -> DynamicUnit ``` #### `scaled` ```kex scaled(factor: Float, notation: String) -> Result ``` ## record `DynamicMeasure` **Fields** - `canonical` : [Float](../number.md#make-float) - `unit` : [DynamicUnit](#record-units-dynamic-dynamicunit) ### Functions #### `value` ```kex value : Float ``` Display magnitude is derived, never a second independently stored value. #### `convertTo` ```kex convertTo(target: DynamicUnit) -> Result ``` #### `+` ```kex +(other: DynamicMeasure) -> Result ``` #### `-` ```kex -(other: DynamicMeasure) -> Result ``` #### `*` ```kex *(other: DynamicMeasure) -> Result ``` #### `/` ```kex /(other: DynamicMeasure) -> Result ``` #### `pow` ```kex pow(exponent: Integer) -> Result ``` #### `scale` ```kex scale(factor: Float) -> DynamicMeasure ``` #### `compareTo` ```kex compareTo(other: DynamicMeasure) -> Result ``` Ordering ignores display preferences but requires compatible dimensions. #### `scalar` ```kex scalar : Result ``` ## type `UnitError` **Variants** - `InvalidScale(Float)` - `ScaleOverflow` - `DimensionMismatch(Dimension, Dimension)` --- --- package: prelude version: "0.4.0-rc.3-dev" source: units/si.kex title: Units.SI entities: - { kind: module, name: "Units.SI" } --- # Units.SI ## module `Units.SI` SI units: metres, grams, watts, volts and the rest, with prefixes and dimensional arithmetic. Opt-in: nothing here is in scope until `using Units.SI`. ```kex using Units.SI main do IO.printLine(3.kilowatt.to(String)) # prints: 3.0 kW IO.printLine(5000.meter.to(String, in: Kilo(Meter)).or("")) # prints: 5.0 km IO.printLine((100.meter / 10.second).to(String)) # prints: 10.0 m/s end ``` Every value is a `Measure` from the prelude, so the arithmetic, conversion and comparison described there apply unchanged. What this module adds is the SI vocabulary (including fused spellings such as `kilometer`), the prefixes as units of their own (`Kilo(Meter)`, for `convertTo` and `to(String, in:)`), and a table of which dimension results from multiplying or dividing two others, so `2.newton * 3.meter` answers in joules and `100.meter / 10.second` in metres per second. ### `meter` ```kex meter(value: Number) -> Measure ``` `value` metres, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the length **Examples** ```kex 5000.meter.to(String) # => Just("5000.0 m") 5000.meter.to(String, in: Kilo(Meter)) # => Just("5.0 km") ``` ### `gram` ```kex gram(value: Number) -> Measure ``` `value` grams, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the mass **Examples** ```kex 500.gram.to(String) # => Just("500.0 g") ``` ### `kelvin` ```kex kelvin(value: Number) -> Measure ``` `value` kelvin, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the temperature **Examples** ```kex 300.kelvin.to(String) # => Just("300.0 K") ``` ### `liter` ```kex liter(value: Number) -> Measure ``` `value` litres, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the volume **Examples** ```kex 2.liter.to(String) # => Just("2.0 L") ``` ### `newton` ```kex newton(value: Number) -> Measure ``` `value` newtons, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the force **Examples** _Force times distance is energy_ ```kex (2.newton * 3.meter).to(String) # => Just("6.0 J") ``` ### `joule` ```kex joule(value: Number) -> Measure ``` `value` joules, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the energy **Examples** ```kex 100.joule.to(String) # => Just("100.0 J") ``` ### `watt` ```kex watt(value: Number) -> Measure ``` `value` watts, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the power **Examples** ```kex 3.kilowatt.to(String) # => Just("3.0 kW") 1500.watt.to(String, in: Kilo(Watt)) # => Just("1.5 kW") ``` _Power times time is energy_ ```kex (3.kilowatt * 2.hour).to(String) # => Just("21600000.0 J") ``` ### `volt` ```kex volt(value: Number) -> Measure ``` `value` volts, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the voltage **Examples** ```kex 12.volt.to(String) # => Just("12.0 V") ``` ### `ampere` ```kex ampere(value: Number) -> Measure ``` `value` amperes, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the current **Examples** _Voltage times current is power_ ```kex (12.volt * 2.ampere).to(String) # => Just("24.0 W") ``` ### `ohm` ```kex ohm(value: Number) -> Measure ``` `value` ohms, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the resistance **Examples** ```kex 470.ohm.to(String) # => Just("470.0 Ω") ``` ### `coulomb` ```kex coulomb(value: Number) -> Measure ``` `value` coulombs, as a `Measure`. **Parameters** - `value` — the quantity **Returns**: the charge **Examples** ```kex 3.coulomb.to(String) # => Just("3.0 C") ``` ### `per` ```kex per(measure: Measure, other: Measure) -> Measure ``` Divides one measure by another, naming the resulting dimension. The spelled-out form of `/`: `100.meter.per(10.second)` and `100.meter / 10.second` are the same call. Metres over seconds is speed, energy over time is power, force over area is pressure: the dimension table decides, and the symbol follows it. **Parameters** - `measure` — the numerator - `other` — the denominator **Returns**: the quotient, in its derived unit **Examples** ```kex 100.meter.per(10.second).to(String) # => Just("10.0 m/s") ``` ### `times` ```kex times(measure: Measure, other: Measure) -> Measure ``` Multiplies one measure by another, naming the resulting dimension. The spelled-out form of `*`. Force times distance is energy, voltage times current is power, power times time is energy. **Parameters** - `measure` — the first factor - `other` — the second factor **Returns**: the product, in its derived unit **Examples** ```kex 2.newton.times(3.meter).to(String) # => Just("6.0 J") (12.volt * 2.ampere).to(String) # => Just("24.0 W") ``` ## type `SIUnit` The SI units this module names. Each carries its dimension (`:length`, `:mass`, `:power`, …) and its symbol, which is what a `Measure` built from it displays with. **Variants** - `Meter` - `Gram` - `Kelvin` - `Liter` - `Newton` - `Joule` - `Watt` - `Volt` - `Ampere` - `Ohm` - `Coulomb` Implements [`Unit`](../units.md#trait-unit). ### Functions #### `factor` (from Unit) ```kex factor(_) ``` #### `kind` (from Unit) ```kex kind(_) ``` #### `symbol` (from Unit) ```kex symbol(_) ``` #### `*` ```kex *(_, _) -> UnitDefinition ``` ## type `SIPrefix` A decimal prefix applied to a unit, which is a unit itself. `Kilo(Meter)` is the kilometre and `Kilo(Watt * Hour)` the kilowatt-hour. Like any other `Unit`, one can be converted to with `convertTo` or displayed in with `to(String, in:)`. Writing a measure in one is spelled out instead: `5.kilometer`. ```kex 5000.meter.convertTo(Kilo(Meter)) # => Ok(5.0 km) 1500.watt.to(String, in: Kilo(Watt)) # => Just("1.5 kW") ``` **Variants** - `Kilo(Unit)` - `Mega(Unit)` - `Giga(Unit)` - `Milli(Unit)` - `Micro(Unit)` - `Nano(Unit)` Implements [`Unit`](../units.md#trait-unit). ### Functions #### `factor` (from Unit) ```kex factor(_) ``` #### `kind` (from Unit) ```kex kind(_) ``` #### `symbol` (from Unit) ```kex symbol(_) ``` ## extends `UnitDefinition` More functions for [`UnitDefinition`](../units.md#record-unitdefinition), added by this module. Implements [`Unit`](../units.md#trait-unit). ### `factor` (from Unit) ### `kind` (from Unit) ### `symbol` (from Unit) ## extends `Measure` More functions for [`Measure`](../units.md#record-measure), added by this module. Displaying a measure in a prefixed unit: the prelude's `to(String, in:)`, for this module's units. ### `factor` ```kex factor : Float ``` ### `kind` ```kex kind : Atom ``` ### `symbol` ```kex symbol : String ``` ### `to` ```kex to(String: Type, in: SIPrefix, format: NumberFormat) -> String? ``` Renders the measure in a prefixed unit. Answers `None` when the measure and the target unit are of different dimensions: you cannot express watts in metres, and this says so rather than producing a wrong number. **Parameters** - `String` — the target type - `in` — the prefixed unit to display in **Returns**: the rendered measure, or `None` **Examples** ```kex 5000.meter.to(String, in: Kilo(Meter)) # => Just("5.0 km") 1500.watt.to(String, in: Kilo(Watt)) # => Just("1.5 kW") ``` _A mismatched dimension answers None_ ```kex 1500.watt.to(String, in: Kilo(Meter)) # => None ``` ### `rescaled` ```kex rescaled(factor: Float, kind: Atom, symbol: String) -> Measure? ``` The measure in a unit of `factor`, or None across dimensions. ### `prefixScaled` ```kex prefixScaled(factor: Float) -> Number ``` Divides the canonical value by a prefix factor. The conversion is exact in principle (0.000005 W is 5 μW) but neither the measured value nor the prefix factor is exactly representable in binary, so the quotient lands an ulp or two off (5.000000000000001). Round back to 15 significant digits, comfortably inside a double's ~15.95, so prefixed measures read the way they were written. The result is always a Float; the annotation says Number because `Integer / Float` infers Integer. ### `*` ```kex *(other: Measure) -> Measure ``` ### `/` ```kex /(other: Measure) -> Measure ``` ### `product` ```kex product(other) -> Measure ``` ### `quotient` ```kex quotient(other) -> Measure ``` ### `productKind` ```kex productKind(left, _) ``` ### `quotientKind` ```kex quotientKind(left, _) ``` ### `productSymbol` ```kex productSymbol(_, left, right) ``` ### `quotientSymbol` ```kex quotientSymbol(_, left, right) ``` --- --- package: tey version: "0.2.1-dev" source: main.kex title: Main entities: --- # Main --- --- package: tey version: "0.2.1-dev" source: tey/broker.kex title: Tey.Broker entities: - { kind: module, name: "Tey.Broker" } --- # Tey.Broker ## module `Tey.Broker` ### `grants` ```kex grants(plugin: Plugin, workspaceRoot: String) -> Grants ``` ### `answered` ```kex answered(grants: Grants, line: String) -> String ``` One request line in, one response line out. Never raises: a request that cannot be read or is refused is answered, so the plugin can say why. ### `host` ```kex host(url: String) -> Result ``` The host name of an http(s) URL, lower-cased; an error for anything else, or for a URL that hides its host behind user information. ## record `Grants` The plugin host's side of a command's conversation. A plugin command runs in a sandbox where it can write nothing and reach no network. Whatever it wants done in the world it asks for, one JSON object per line on its stdout, and waits for the answer on its stdin: ```kex {"op":"print","text":"…"} {"op":"read","path":"README.md"} {"op":"write","path":".github/workflows/ci.yml","content":"…"} {"op":"delete","path":"old.yml"} {"op":"run","command":"git","arguments":["status","--short"]} {"op":"fetch","method":"GET","url":"https://api.github.com/…","body":""} ``` Each request is checked here against what the plugin declared and the user approved, then carried out by Tey or refused: ```kex {"ok":true, …} {"ok":false,"error":"…"} ``` So `Process.Run(["git"])` means git and nothing else, and `Net.Connect(["api.github.com"])` means that host and no other — the plugin never runs a program with the user's access or opens a socket; Tey does, on its behalf, after looking. What a plugin was approved for. **Fields** - `workspaceRoot` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `writes?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) - `commands` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `hosts` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) --- --- package: tey version: "0.2.1-dev" source: tey/cache.kex title: Tey.Cache entities: - { kind: module, name: "Tey.Cache" } --- # Tey.Cache ## module `Tey.Cache` ### `root` ```kex root : String ``` ### `packagePath` ```kex packagePath(dep: Tey.Lockfile.Dependency) -> String ``` Include the source URL in the cache identity: two packages with the same display name must never share a checkout. ### `repositoryPath` ```kex repositoryPath(dep: Tey.Lockfile.Dependency) -> String ``` ### `fetch` ```kex fetch(dep: Tey.Lockfile.Dependency) -> Result ``` ### `verify` ```kex verify(dep: Tey.Lockfile.Dependency) -> Result ``` --- --- package: tey version: "0.2.1-dev" source: tey/cli.kex title: Tey.Cli entities: - { kind: module, name: "Tey.Cli" } --- # Tey.Cli ## module `Tey.Cli` ### `parser` ```kex parser : OptionConfig ``` Options AND commands in one declaration: OptionParser matches the command words, strips them, and hands the handler what is left. Tey keeps no dispatcher of its own, so the help text and what actually runs cannot drift apart — they are read off the same list. `foul` because the handlers do things. ### `cli` ```kex cli : OptionConfig ``` The parser Tey actually runs: the declared commands, plus one for every `command(...)` this package's manifest declares. Registering them here rather than in a fallback is what gets them into `tey help`, into the unknown-command message, and into argument stripping, for free. A package command can never shadow a built-in: the manifest reader refuses those names outright (Tey.Manifest.reservedCommands). They are listed under their own help heading regardless — "unknown command" should send a reader to package.kex or to Tey, and the two lists are written by different people. ### `dispatch` ```kex dispatch(args: [String]) -> Integer ``` The line is parsed once here to answer the two questions `run` cannot — is this `--version`, and does it name a command Tey already answers — and then handed to `run`, which parses it again and dispatches. A parse ERROR is left to `run` as well: reporting it with the help text is the one policy every tool built on OptionParser shares. ### `colorFlag?` ```kex colorFlag?(argument: String) -> Bool ``` Whether an argument turns colors off. `--no-colors` is how `kex` spells it, and somebody switching between the two tools should not have to remember which one takes the `s`. ### `plain?` ```kex plain?(args: [String]) -> Bool ``` Whether the line asks for plain output. Only Tey's own arguments count: past a bare `--` they belong to the program being run, which may well have a `--no-color` of its own. ### `withoutColorFlags` ```kex withoutColorFlags(args: [String]) -> [String] ``` The line without the color flags, which are answered before parsing — by setting NO_COLOR, the one switch `Console`, every child `kex`, and this parser's own error text all read. Taking them out here rather than leaving them to the parser is what makes the flag work wherever it is written: `tey docs` is a passthrough command whose own parser has never heard of it. ### `dispatchArguments` ```kex dispatchArguments(args: [String]) -> Integer ``` ### `relayColors` ```kex relayColors : Void ``` `kex` colors only a terminal. Every `kex` Tey starts — `tey test`, `tey run`, a package command — now writes to Tey's own stdout (`Process.stream` hands its child the descriptors), so it sees the terminal Tey sees and colors by itself; until it did, it wrote through a pipe and had to be told. Telling it still agrees with what it sees. NO_COLOR, when set, is left to win in every child; `Kex.AST` passes `--no-colors`, which outranks this, so nothing Tey parses back picks up escape codes. ### `announceToolchain` ```kex announceToolchain : Void ``` Tells the rest of the process which compiler Tey selected. `Kex.AST` — which is how a `package.kex` is read at all — shells out to the compiler named by `$KEX`, falling back to whatever `kex` PATH happens to hold. Tey is started by `erl`, not by `kex`, so nothing had set that: a manifest was parsed by an unrelated compiler if one was on PATH, and by none at all otherwise — which is why a package's commands silently vanished from `tey help` on a machine where `kex` was installed only inside the Tey home. An explicit `$KEX` is left alone: someone who set it means it, and that is the documented override the compiler itself honours. --- --- package: tey version: "0.2.1-dev" source: tey/commands.kex title: Tey.Commands entities: - { kind: module, name: "Tey.Commands" } --- # Tey.Commands ## module `Tey.Commands` ### `manifest` ```kex manifest : Result ``` ### `lock` ```kex lock : Integer ``` ### `add` ```kex add(name: String, options: ParsedOptions) -> Integer ``` ### `localOverride` ```kex localOverride(name: String, path: String, remove?: Bool, list?: Bool) -> Integer ``` ### `install` ```kex install(without: String, packageName: String = …) -> Integer ``` `--without dev` leaves a group's dependencies out of this install. The LOCKFILE still lists them — what is omitted is the fetching, so switching the flag off later needs no re-resolve. ### `installWorkspace` ```kex installWorkspace(without: String, packageName: String, force?: Bool, approveAll?: Bool) -> Integer ``` `install`, where `force?` lets targets replace programs Tey does not own (see Tey.Programs.refusal). ### `installDependencies` ```kex installDependencies(context: Context, without: String, approveAll?: Bool) -> Result ``` Resolves (or reuses) the workspace's lock, fetches and verifies what it names, approves plugins, and writes the lock back — everything `tey install` does before building. ### `fetchAll` ```kex fetchAll(dependencies: [Tey.Lockfile.Dependency]) -> Result ``` ### `installPrograms` ```kex installPrograms(arguments: [String], options: InstallOptions) -> Integer ``` `tey install ...`: a program from a path or a Git URL, or installed programs updated by name. Several arguments must all be names — a source is one program, with its own options. ### `installProgram` ```kex installProgram(argument: String, options: InstallOptions) -> Integer ``` ### `reinstallProgram` ```kex reinstallProgram(name: String, options: InstallOptions) -> Integer ``` `tey install `: installs an installed program again from where its receipt says it came from — which is what updating it means. A tag or an exact commit stays pinned unless a new `--tag`/`--branch`/`--ref` is given; a branch (or HEAD) and a path are rebuilt only when their commit or content moved, unless `--rebuild` says to anyway (a new Erlang, a compiler fix). ### `uninstallPrograms` ```kex uninstallPrograms(names: [String]) -> Integer ``` `tey uninstall ...`: removes the programs each receipt owns, then the receipt and the build tree it came from. A program that has since been replaced by a file Tey did not write is left where it is, and said so. ### `uninstallProgram` ```kex uninstallProgram(name: String) -> Integer ``` ### `installFromPath` ```kex installFromPath(path: String, options: InstallOptions) -> Integer ``` Installs the program at `path` without touching it: the workspace around it is copied into a staging directory, which gets its own lock, dependencies and build, and only replaces the program's previous build tree once its programs are in place. The user's checkout gains no `ebin/`, no `tey.lock`, nothing. ### `installFromGit` ```kex installFromGit(url: String, options: InstallOptions) -> Integer ``` Installs a program from a Git repository at the commit `--tag`, `--branch` or `--ref` names (the default branch's HEAD otherwise). The commit is fetched into the cache the way a dependency is, and installed from there as a path is — the cache checkout itself is never built in. ### `previousApprovals` ```kex previousApprovals(name: String) -> {String: String} ``` The plugin approvals the previous install of program `name` was built with: its build tree's lock. None for a first install. ### `programMember` ```kex programMember(context: Context, packageName: String) -> Result ``` The member of `context` a program install means: the one `--package` names, else the one the path is inside of. ### `installProgramMember` ```kex installProgramMember(context: Context, member: Member, options: InstallOptions, origin: Receipt) -> Integer ``` The shared half of a path and a Git install, once the member is known: refuse a library, check `--target`, stage, build, publish. ### `installStaged` ```kex installStaged(context: Context, member: Member, staging: String, options: InstallOptions, origin: Receipt) -> Integer ``` ### `updateDependencies` ```kex updateDependencies(name: String = …) -> Integer ``` ### `dependencyClosure` ```kex dependencyClosure(state: LockState, roots: [String]) -> [String] ``` Names affected by a member-local or named update, including transitive dependency edges recorded in the newly resolved graph. ### `mergeUpdate` ```kex mergeUpdate(previous: LockState, fresh: LockState, roots: [String], wholeWorkspace?: Bool) -> LockState ``` Publishes fresh workspace metadata and selected resolutions while retaining unrelated external pins. Existing approvals are carried forward here and subsequently filtered against the visible declarations by approvePlugins. ### `lockReusable?` ```kex lockReusable?(state: LockState, manifestFingerprint: String, toolchain: Tey.Toolchain.ToolchainInfo) -> Bool ``` ### `validateUpdateName` ```kex validateUpdateName(members: [Member], name: String) -> Result ``` ### `updateScopeMembers` ```kex updateScopeMembers(context: Context) -> [Member] ``` ### `installTargetsSelected` ```kex installTargetsSelected(context: Context, packageName: String, force?: Bool = …) -> Integer ``` ### `installMemberTargets` ```kex installMemberTargets(member: Member, force?: Bool = …) -> Integer ``` Puts this package's `target(...)` executables where they can be run by name, and records them in the package's receipt (see Tey.Programs). `${TEY_HOME}/bin` is a directory Tey already owns and already asks people to put on PATH — it is how the `kex` shim gets there — so a package's programs land beside it rather than needing a second directory and a second instruction. Only packages that DECLARE a target are affected. A library has none, so `tey install` in one still means exactly what it meant before: fetch the dependencies. Every name is checked before anything is built, so a refused install costs nothing and changes nothing. ### `installMemberTargetsFrom` ```kex installMemberTargetsFrom(member: Member, force?: Bool, origin: Receipt, selected: [String]) -> Integer ``` `installMemberTargets`, recording `origin`'s source fields in the receipt — a program built in a staging copy came from somewhere else. `selected` limits the targets installed (`--target`); empty means all. ### `releaseTarget` ```kex releaseTarget(owner: String, name: String) -> Result ``` Drops `name` from `owner`'s receipt after another package took it over with `--force`; a receipt left owning nothing is removed. ### `recordInstall` ```kex recordInstall(package: ManifestPackage, origin: Receipt, installed: [String], receipts: [Receipt], claims: [(String, Claim)]) -> Result ``` Writes the package's receipt and settles ownership around it: names taken over with `--force` leave the other package's receipt, and targets the previous receipt owned that this install no longer declares are removed from the bin directory rather than left behind unowned. ### `dependencyRoots` ```kex dependencyRoots : [String] ``` `--source-root` for every fetched dependency. Without these a dependency is just bytes in the cache: `using Greet` cannot find it, and the package that declared it does not compile. A dependency the lockfile names but the cache does not have is reported rather than quietly dropped — the answer is `tey install`, and silence would look like a broken `using`. ### `dependencyRootsAt` ```kex dependencyRootsAt(start: String) -> [String] ``` `dependencyRoots` for the workspace around `start` rather than around the working directory. A program being installed from a path is built in a staged copy while the user stands somewhere else entirely — possibly in another project, whose lockfile would otherwise supply the dependencies. ### `sourceDirectoryAt` ```kex sourceDirectoryAt(packageRoot: String) -> String ``` The directory a package's module names are resolved against, relative to it: `src`, or the package itself when its package.kex names the folders at its top that hold modules (`sources(["app", "config", "db"])` — then `App.Models.User` is `./app/models/user.kex`). A manifest that cannot be read means `src`; every caller reports that in its own way soon enough. ### `sourceUnit` ```kex sourceUnit(directory: String, path: String) -> String ``` A file of a package's source directory, as the compiler is to be given it: relative to the package. A root of "." adds nothing in front. ### `sourceUnit?` ```kex sourceUnit?(path: String) -> Bool ``` Whether a file directly under the source directory is a module to compile. With the package itself as the root, its manifests sit there too, and are not modules. ### `sourceRoots` ```kex sourceRoots : [String] ``` Every `--source-root` a compile of THIS package needs: its own source directory first (`src/`, or the package itself when it declares `sources`), then each dependency's. Own-first is deliberate — a package that shadows a dependency's module name means its own, the same way `--source-root` order already decides that for dependencies among themselves. ### `sourceRootsAt` ```kex sourceRootsAt(packageRoot: String) -> [String] ``` ### `librarySources` ```kex librarySources : [String] ``` A library's compilable units: every `.kex` directly under its source directory (`src/`, or the package's top when it declares `sources`). Not a recursive walk — a nested module is reached by the file that declares it, and compiling it twice by name is how you get a duplicate-module error out of a perfectly good package. ### `librarySourcesAt` ```kex librarySourcesAt(packageRoot: String) -> [String] ``` Returns units relative to `packageRoot`, not absolute: the compiler double-registers a module's declarations when the entry file it is asked to compile is an absolute path that also falls under an explicit `--source-root` (kexhq/kex#?, surfaced by `tey build`ing Rodolfo — a `private do` function building a record literal of a type carrying earlier `make` methods reported that type's method "defined twice"). A relative unit, resolved through the working directory `streamAt`/`runAt` `cd` into, does not collide with the source-root the same way. Callers must invoke the compiler with `packageRoot` as the working directory. ### `backendFlags` ```kex backendFlags(interpret?: Bool) -> [String] ``` What `tey build` produces is what ships, so what `tey run` and `tey test` exercise has to be the same thing — which is what `kex` does with no backend flag at all: it compiles and runs on the BEAM. `-R` tree-walks the source instead, and processes, `BEAM.*` interop and the String/Char representation differ enough between the two that a green interpreter run says nothing about the built package. `--interpret` is the way back, for a backend gap or a debugging session. ### `runPackage` ```kex runPackage(extra: [String], interpret?: Bool, packageName: String = …) -> Integer ``` ### `runCommand` ```kex runCommand(command: Command, extra: [String], interpret?: Bool) -> Integer ``` `tey ` for a `command(...)` the manifest declares. A `.kex` script is run through the SELECTED toolchain with this package's source roots already in place — the same environment `tey run` gives, so a release script can `using` the package it is releasing. Anything else is a shell line: `sh -c` is what makes `command("fmt", run: "kex --format src")` behave the way the same line behaves in a terminal, pipes and `&&` included. Trailing arguments are passed as "$@" rather than pasted into the string, so `tey fmt --check` cannot be re-split by the shell. The `--` before the arguments plays the same role as in runPackage. ### `generate` ```kex generate(arguments: [String]) -> Integer ``` The locked dependencies, then the programs Tey has installed. Outside a project there are no dependencies to show, so it is the programs alone rather than an error about a missing package.kex. `tey generate : [--option value ...] [--force]` Runs a generator a direct dependency declares in its `plugin` block. The plugin is a child process: it is handed the invocation as one line of JSON on stdin and answers with the files it wants written, which Tey — not the plugin — then writes, inside the workspace and asking before it replaces anything (`--force` answers yes). The child itself runs in the operating system's sandbox: it cannot write outside scratch space or use the network (`--unconfined` lifts that, with a warning). ### `pluginCommand` ```kex pluginCommand(arguments: [String]) -> Integer ``` `tey plugin : [--option value ...]` Runs a command a direct dependency declares in its `plugin` block, confined to what the plugin declared and the user approved. ### `list` ```kex list : Integer ``` ### `listPrograms` ```kex listPrograms(programs: [Receipt]) -> Void ``` ### `build` ```kex build : Integer ``` ### `buildSelected` ```kex buildSelected(packageName: String, workspace?: Bool, requestedJobs: String = …) -> Integer ``` ### `buildMember` ```kex buildMember(member: Member) -> Integer ``` ### `testReportingFlags` ```kex testReportingFlags(json?: Bool, list?: Bool, only: String) -> [String] ``` How `tey test` should REPORT, as compiler flags (kexhq/kex#199). `--json` and `--list` are for a tool reading the run rather than a person watching it: one JSON record per case, carrying the file and line of the `it` and of the failure, and — with `--list` — the cases discovered without any of them being run. `--only ` narrows the run to one case or one group. Tey does not interpret any of it: the flags are the compiler's, the records are the compiler's, and this is the mapping from what a person typed to what the compiler is asked. Which is the point — an editor drives `tey test` because Tey knows the package's source roots, not because it wants Tey to reformat the answer (docs/testing.md). ### `testSpecs` ```kex testSpecs(requested: [String]) -> [String] ``` The spec files to run: the ones named on the line, or every `spec/*.spec.kex` when none were. Naming one is what an editor's per-file ▶ needs — and what anyone debugging a single suite in a package with thirty of them wants. ### `test` ```kex test(requested: [String], interpret?: Bool, json?: Bool, list?: Bool, only: String) -> Integer ``` ### `testSelected` ```kex testSelected(requested: [String], packageName: String, workspace?: Bool, interpret?: Bool, json?: Bool, list?: Bool, only: String, requestedJobs: String = …) -> Integer ``` ### `testAt` ```kex testAt(packageRoot: String, requested: [String], interpret?: Bool, json?: Bool, list?: Bool, only: String) -> Integer ``` ### `clean` ```kex clean(all?: Bool) -> Integer ``` ### `setupMergeDriver` ```kex setupMergeDriver(remove?: Bool) -> Integer ``` ### `new` ```kex new(directory: String, library: Bool) -> Integer ``` `tey new ` — a package in a directory of its own, which the command creates. The pair with `init` below follows what `git init` and `cargo new` already taught everyone: `new` makes the directory, `init` adopts the one you are standing in. ### `init` ```kex init(name: String, library: Bool) -> Integer ``` `tey init` — a package in the CURRENT directory, named after it. This is the half that was missing: a repository someone had already made and cloned had no way to become a package except `tey new` in a temp directory and moving the files over by hand. The name is overridable because a directory name is not always the package name — `kexhq/greet` checked out as `greet-main` is still `greet`. ## record `WorkspaceRunState` **Fields** - `runStatus` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) (optional) - `failedMembers` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) --- --- package: tey version: "0.2.1-dev" source: tey/generator.kex title: Tey.Generator entities: - { kind: module, name: "Tey.Generator" } --- # Tey.Generator ## module `Tey.Generator` ### `decode` ```kex decode(text: String) -> Result<[Operation], String> ``` Decodes the child-to-host half of the separately versioned protocol. Plugin stdout is data, never Kex source evaluated in Tey's process. ### `apply` ```kex apply(operations: [Operation], workspaceRoot: String, force?: Bool) -> Result ``` ### `safePath` ```kex safePath(workspaceRoot: String, path: String) -> Result ``` ## type `Operation` **Variants** - `WriteFile(String, String)` - `DeleteFile(String)` --- --- package: tey version: "0.2.1-dev" source: tey/git.kex title: Tey.Git entities: - { kind: module, name: "Tey.Git" } --- # Tey.Git ## module `Tey.Git` ### `execute` ```kex execute(args: [String]) -> Result ``` ### `pinRef` ```kex pinRef(url: String, selector: String, requested: String) -> Result<(String, String), String> ``` ### `tags` ```kex tags(url: String) -> Result<[String], String> ``` Every version-shaped tag a repository publishes, newest first. Used to answer a range requirement, and to list installable Kex toolchains. ### `commitId?` ```kex commitId?(text: String) -> Bool ``` A full hexadecimal commit id. A short one is left to `ls-remote`: it could as well be a ref name. --- --- package: tey version: "0.2.1-dev" source: tey/host.kex title: Tey.Host entities: - { kind: module, name: "Tey.Host" } --- # Tey.Host ## module `Tey.Host` ### `packageManager` ```kex packageManager : String ``` The machine Tey runs on, as far as telling someone how to fix it goes: which package manager installs what is missing. Tey never runs these commands — installing system packages needs privileges Tey should not ask for — it only prints the one line that does it, so the advice is exact rather than "install GMP somehow". The package manager this system uses: "brew", "apt", "dnf", "pacman", or "" when it cannot be told. Linux is judged by /etc/os-release's ID plus everything ID_LIKE names, which is what gives Linux Mint and Pop!_OS Ubuntu's answer. ### `installCommand` ```kex installCommand(manager: String, packages: [String]) -> String? ``` The command installing `packages` with this system's package manager, or None when there is no manager to name. Package names differ between managers, so the caller passes the right list for `packageManager`. ### `librariesHint` ```kex librariesHint : String ``` The shared libraries a released Kex binary loads: readline, GMP, PCRE2 and (on Linux) OpenSSL 3. Checked against each distro's own package names. ### `gitHint` ```kex gitHint : String ``` Git: Tey lists Kex releases and fetches every Git dependency with it. --- --- package: tey version: "0.2.1-dev" source: tey/local.kex title: Tey.Local entities: - { kind: module, name: "Tey.Local" } --- # Tey.Local ## module `Tey.Local` ### `load` ```kex load(context: Context) -> Result<[Override], String> ``` ### `set` ```kex set(context: Context, name: String, path: String) -> Result ``` ### `remove` ```kex remove(context: Context, name: String) -> Result ``` ### `validateAgainstLock` ```kex validateAgainstLock(overrides: [Override], lock: Tey.Lockfile.LockState) -> Result ``` ## record `Override` **Fields** - `overrideName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `overridePath` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `overrideFile` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `overrideManifest` : [ManifestPackage](../tey/manifest.md#record-tey-manifest-manifestpackage) --- --- package: tey version: "0.2.1-dev" source: tey/lockfile.kex title: Tey.Lockfile entities: - { kind: module, name: "Tey.Lockfile" } --- # Tey.Lockfile ## module `Tey.Lockfile` Pure, and declared so. Reading and writing `tey.lock` is somebody else's job — this module only turns the text into a LockState and back, which is what makes the whole format testable without a disk (tey/spec/lockfile.spec.kex). ### `encode` ```kex encode(lock: LockState) -> String ``` ### `dependencyName` ```kex dependencyName(dep: Dependency) -> String ``` ### `dependencyGit` ```kex dependencyGit(dep: Dependency) -> String ``` ### `dependencyCommit` ```kex dependencyCommit(dep: Dependency) -> String ``` ### `dependencySha256` ```kex dependencySha256(dep: Dependency) -> String ``` ### `dependencyGroups` ```kex dependencyGroups(dep: Dependency) -> [String] ``` ### `dependencyListLine` ```kex dependencyListLine(dep: Dependency) -> String ``` ### `decode` ```kex decode(text: String) -> Result ``` ## record `Dependency` **Fields** - `name` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `version` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `source` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `git` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `resolved` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `commit` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `subdir` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `path` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sha256` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `groups` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `dependencies` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `pluginFingerprints` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)} (optional) - `workspace?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) ## record `LockState` **Fields** - `kexRequirement` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `kexVersion` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `otpRequirement` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `otpRelease` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) (optional) - `workspaceMembers` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)} (optional) - `manifestFingerprint` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `lockedDependencies` : [Dependency] - `pluginApprovals` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)} (optional) --- --- package: tey version: "0.2.1-dev" source: tey/manifest.kex title: Tey.Manifest entities: - { kind: module, name: "Tey.Manifest" } --- # Tey.Manifest ## module `Tey.Manifest` ### `sourceFolder?` ```kex sourceFolder?(name: String) -> Bool ``` The command names Tey answers itself. A `command(...)` may not take one of them: `tey test` must mean the same thing in every checkout, so a manifest that tries to redefine it is an error at read time rather than a silent shadow. Kept here because the READER is what enforces it, and Tey.Cli cannot be asked: it is built on this module, so reading the list off its declared commands would be a cycle. The two have to be kept in step by hand — one `.command(...)` in Tey.Cli, one name here (the first word is enough: `kex` covers every `kex ...` subcommand). Whether a name can be a source folder: one directory at the package's top, by its plain name. Not a path — its name is the first part of every module in it — and nothing hidden. ### `reservedCommands` (constant) ```kex reservedCommands : [String] ``` ### `read` ```kex read(text: String) -> Result ``` Reads a `package.kex`. The compiler parses the manifest. Tey only interprets the resulting Kex AST, so comments, multiline calls, strings, and future grammar fixes cannot drift from the language that actually compiles package.kex. ### `readWorkspace` ```kex readWorkspace(text: String) -> Result ``` Reads a virtual-workspace manifest: a package.kex whose root is `workspace` rather than a publishable package. Package-root workspaces are returned on ManifestPackage.manifestWorkspace by `read` above. ### `readLocal` ```kex readLocal(text: String) -> Result<[LocalOverride], String> ``` ## type `DependencySource` **Variants** - `WorkspaceSource` - `GitSource(String, String, String, String)` - `PathSource(String)` ## record `Dependency` **Fields** - `manifestName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `manifestSource` : [DependencySource](#type-tey-manifest-dependencysource) - `manifestGroups` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) ## record `ManifestWorkspace` **Fields** - `memberPatterns` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) ## record `LocalOverride` **Fields** - `localName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `localPath` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `Target` **Fields** - `name` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `entrypoint` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `PluginCapability` **Fields** - `capabilityKind` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `capabilityValues` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) ## record `PluginOption` **Fields** - `optionName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `optionType` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `PluginOperation` **Fields** - `operationName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `operationEntrypoint` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `operationOptions` : [[PluginOption](#record-tey-manifest-pluginoption)] (optional) ## record `Plugin` **Fields** - `pluginNamespace` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `pluginTeyRequirement` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `pluginCapabilities` : [[PluginCapability](#record-tey-manifest-plugincapability)] (optional) - `pluginCommands` : [[PluginOperation](#record-tey-manifest-pluginoperation)] (optional) - `pluginGenerators` : [[PluginOperation](#record-tey-manifest-pluginoperation)] (optional) ## record `Command` A project-specific command, run as `tey `. Without these every package grows a Makefile or a bin/ script beside package.kex for "the way you run the linter", which the manifest — the one file a newcomer reads — says nothing about. A name is one word, grouped with a colon: `db:migrate`, `assets:build`. **Fields** - `commandName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `commandRun` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `commandDescription` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `Toolchain` A toolchain package: the compiler executable plus the runtime and standard library it was built with, installed together so the three can never drift apart. Kex's own `package.kex` is the one that declares it — and a reader that did not know the declaration could not run any tey command from the compiler's own checkout, which is where it is most used. Fields are prefixed like Command's: a bare `compiler` would be read as a method on an unpinned receiver before it is read as a field here. **Fields** - `toolchainName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `toolchainCompiler` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `toolchainRuntime` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `toolchainStdlib` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `ManifestPackage` **Fields** - `name` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `version` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `description` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `license` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `packageKexRequirement` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `packageOtpRequirement` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `entrypoint` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceFolders` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `manifestDependencies` : [Dependency] (optional) - `targets` : [[Target](#record-tey-manifest-target)] (optional) - `commands` : [[Command](#record-tey-manifest-command)] (optional) - `manifestToolchain` : Toolchain? (optional) - `manifestWorkspace` : [ManifestWorkspace](#record-tey-manifest-manifestworkspace)? (optional) - `plugins` : [[Plugin](#record-tey-manifest-plugin)] (optional) --- --- package: tey version: "0.2.1-dev" source: tey/plugin.kex title: Tey.Plugin entities: - { kind: module, name: "Tey.Plugin" } --- # Tey.Plugin ## module `Tey.Plugin` ### `fingerprint` ```kex fingerprint(plugin: Plugin) -> String ``` Fingerprints only the declared interface and requested resources. Provider implementation changes do not silently revoke approval; a capability, command schema, generator schema, or supported-Tey change does. ### `fingerprints` ```kex fingerprints(plugins: [Plugin]) -> {String: String} ``` ### `approved?` ```kex approved?(plugin: Plugin, approvals: {String: String}) -> Bool ``` ### `missingApprovals` ```kex missingApprovals(plugins: [Plugin], approvals: {String: String}) -> [Plugin] ``` ### `validApprovals` ```kex validApprovals(plugins: [Plugin], approvals: {String: String}) -> {String: String} ``` Keeps only approvals still backed by an identical visible declaration. This is used when a lock is refreshed: removed plugins and interfaces whose resources changed cannot inherit an old authorization. ### `bindingApproved?` ```kex bindingApproved?(binding: Binding, approvals: {String: String}) -> Bool ``` ### `missingBindingApprovals` ```kex missingBindingApprovals(bindings: [Binding], approvals: {String: String}) -> [Binding] ``` ### `validBindingApprovals` ```kex validBindingApprovals(bindings: [Binding], approvals: {String: String}) -> {String: String} ``` ### `validateScopes` ```kex validateScopes(globals: [Binding], locals: [Binding]) -> Result ``` ### `visibleBinding` ```kex visibleBinding(bindings: [Binding], namespace: String, currentMember: String?) -> Result ``` ### `generator` ```kex generator(binding: Binding, name: String) -> Result ``` ## record `Binding` **Fields** - `approvalKey` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `scopeName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `declaration` : [Plugin](../tey/manifest.md#record-tey-manifest-plugin) --- --- package: tey version: "0.2.1-dev" source: tey/pluginhost.kex title: Tey.PluginHost entities: - { kind: module, name: "Tey.PluginHost" } --- # Tey.PluginHost ## module `Tey.PluginHost` ### `encodeInvocation` ```kex encodeInvocation(invocation: Invocation) -> String ``` The host-to-child half of protocol version 1. It is deliberately ordinary JSON rather than Kex source: a plugin receives data and cannot make Tey evaluate declarations smuggled into its context. ### `typedOptions` ```kex typedOptions(operation: Tey.Manifest.PluginOperation, supplied: {String: String}) -> Result<{String: Any}, String> ``` ## record `Invocation` **Fields** - `invocationKind` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `invocationNamespace` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `invocationOperation` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `workspaceRoot` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `currentMember` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)? (optional) - `members` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)} (optional) - `graph` : [[Tey.Lockfile.Dependency](../tey/lockfile.md#record-tey-lockfile-dependency)] (optional) - `lock` : [Tey.Lockfile.LockState](../tey/lockfile.md#record-tey-lockfile-lockstate) - `toolchain` : [Tey.Toolchain.ToolchainInfo](../tey/toolchain.md#record-tey-toolchain-toolchaininfo) - `arguments` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `options` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): Any} (optional) - `environment` : {[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string): [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)} (optional) --- --- package: tey version: "0.2.1-dev" source: tey/programs.kex title: Tey.Programs entities: - { kind: module, name: "Tey.Programs" } --- # Tey.Programs ## module `Tey.Programs` What Tey has put in `${TEY_HOME}/bin`, and who owns each file there. Every package whose targets Tey installs gets a receipt at `${TEY_HOME}/programs//receipt.json`. The receipt is the only authority for ownership: a file in the bin directory that no receipt lists is not Tey's to overwrite or delete. Without one, a second package declaring the same target name silently replaced the first package's program, and nothing could say what a file in there was or where it had come from. Keyed by PACKAGE, not by binary: one package may install several targets, and reinstalling or removing it acts on all of them together. ### `pinnedReason` ```kex pinnedReason(receipt: Receipt, options: InstallOptions) -> String? ``` Why reinstalling `receipt` by name has nothing to do, or None when it must be rebuilt. A tag or an exact commit is a pin the user chose: it stays, with a pointer at moving it. Pure, so the whole rule is testable. ### `reservedNames` (constant) ```kex reservedNames : [String] ``` Names that are Tey's own and never a package's to take: the `kex` shim lives in this directory, and replacing it would change which compiler every shell runs. ### `directory` ```kex directory : String ``` ### `classify` ```kex classify(argument: String) -> ProgramSource ``` A path when it starts like one (`.`, `/`, `~`), a Git URL when it has a scheme or is scp-like (`git@host:owner/repo`), and otherwise the name of an installed program. ### `buildDirectory` ```kex buildDirectory(name: String) -> String ``` Where a program's staged build tree ends up once it is installed. ### `stagingDirectory` ```kex stagingDirectory : Result ``` A fresh directory to build a program in before it is installed. Beside `programs/` rather than in it, so a half-built tree is never listed as a program, and on the same filesystem, so publishing it is a rename. ### `stage` ```kex stage(root: String, destination: String) -> Result ``` Copies the tree at `root` into `destination`: what a snapshot would hold, plus a committed `tey.lock` — the dependency versions the program's author tested, which a snapshot leaves out. ### `copyInto` ```kex copyInto(root: String, relative: String, destination: String) -> Bool ``` ### `publishBuild` ```kex publishBuild(name: String, staged: String) -> Result ``` Replaces a program's build tree with the one it was just installed from. The old tree is moved aside first and removed last, so a failure part way leaves one of the two in place. ### `receiptPath` ```kex receiptPath(name: String) -> String ``` ### `encode` ```kex encode(receipt: Receipt) -> String ``` ### `decode` ```kex decode(text: String) -> Result ``` ### `claim` ```kex claim(target: String, packageName: String, receipts: [Receipt], exists?: Bool, teyBuilt?: Bool) -> Claim ``` The decision for one target name, given every receipt and what is on disk. Pure, so the whole rule is testable without a bin directory. `teyBuilt?` covers the programs a project-local `tey install` put here before receipts existed: they are escripts Tey itself wrote, so taking them into a receipt loses nothing, where refusing would make every existing installation demand `--force` on its next reinstall. ### `refusal` ```kex refusal(target: String, claim: Claim, path: String, force?: Bool) -> String? ``` Why a claim stops the install, or None when it may go ahead. `--force` takes a name over from another package or from an unowned file, but never from Tey itself. ### `load` ```kex load(name: String) -> Receipt? ``` ### `all` ```kex all : [Receipt] ``` Every receipt, by package name. An unreadable one is reported rather than skipped silently: skipping it would make the files it owns look unowned. ### `save` ```kex save(receipt: Receipt) -> Result ``` Written beside the final file and renamed into place, so a reader never sees half a receipt and a failed write leaves the previous one intact. ### `remove` ```kex remove(name: String) -> Bool ``` ### `teyBuilt?` ```kex teyBuilt?(path: String) -> Bool ``` Whether `path` is an escript Tey's own target build wrote: its emulator line starts Kex's escript launcher, or names `kex_main` directly as an escript built before the launcher did. ### `listLine` ```kex listLine(receipt: Receipt) -> String ``` The line `tey list` prints for one installed program. ### `pinText` ```kex pinText(receipt: Receipt) -> String ``` `v0.4.0`, `main (3f9c1a2)`, or a short commit — what a Git install is pinned to, as `tey list` shows it. ## record `Receipt` **Fields** - `name` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `version` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceKind` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourcePath` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceDigest` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceSelector` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceRequested` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `sourceCommit` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `selectedTargets` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `targets` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `kex` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `otp` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `installedAt` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `InstallOptions` How `tey install ` was asked to install a program: the workspace member (`--package`), the Git pin (`--tag`/`--branch`/`--ref`, "ref" of "HEAD" when none), which targets (`--target`, all when empty), whether to take over others' programs (`--force`), and whether to re-resolve rather than use the program's committed lock (`--fresh`). `pinned?` says a pin was given at all — reinstalling by name keeps the receipt's unless one is. `rebuild?` rebuilds even when nothing moved. **Fields** - `packageName` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `force?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) - `selector` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `requested` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `pinned?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) - `targets` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `fresh?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) - `rebuild?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) - `approvePlugins?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) ## type `ProgramSource` What `tey install ` names. `tey add` is how a dependency is added, so an argument to `tey install` always means a program. **Variants** - `PathSource(String)` - `GitSource(String)` - `InstalledName(String)` ## type `Claim` What installing one target name would do to the bin directory. **Variants** - `Free` - `Mine` - `Adopt` - `Reserved` - `OwnedBy(String)` - `Foreign` --- --- package: tey version: "0.2.1-dev" source: tey/resolver.kex title: Tey.Resolver entities: - { kind: module, name: "Tey.Resolver" } --- # Tey.Resolver ## module `Tey.Resolver` ### `fingerprint` ```kex fingerprint(context: Context) -> String ``` ### `resolveWorkspace` ```kex resolveWorkspace(context: Context) -> Result ``` ### `resolveWorkspaceWithInfo` ```kex resolveWorkspaceWithInfo(context: Context, toolchain: Tey.Toolchain.ToolchainInfo) -> Result ``` The caller may already have inspected the selected compiler while deciding whether an existing lock is reusable. Passing that result through keeps the `--info` contract to one invocation per install/resolve decision. ### `resolvePackage` ```kex resolvePackage(manifest: ManifestPackage) -> Result ``` --- --- package: tey version: "0.2.1-dev" source: tey/sandbox.kex title: Tey.Sandbox entities: - { kind: module, name: "Tey.Sandbox" } --- # Tey.Sandbox ## module `Tey.Sandbox` ### `TOOL` (constant) macOS's `sandbox-exec`. The only mechanism supported so far; on any other system `available?` is false and the caller has to refuse or be told to run unconfined. ### `available?` ```kex available? : Bool ``` ### `scratch` ```kex scratch : [String] ``` The directories a Kex child always needs to write to: where the compiler puts its scratch files and its run cache. ### `profile` ```kex profile(limits: Limits, home: String) -> String ``` The sandbox profile for `limits`. Later rules win, so: allow by default, deny the dangerous classes, then allow back the exceptions. ### `prefix` ```kex prefix(limits: Limits) -> Result<([String], String), String> ``` The words to put in front of a command line to run it under `limits`: `sandbox-exec -f `. The profile file is left for the caller to delete once the child has exited. ## record `Limits` What a plugin process is allowed to do beyond reading files and computing. A plugin is code from a dependency, run on the user's machine. Approval says the user accepts what the plugin *asked* for; this is what makes the answer true, by running the child inside the operating system's sandbox: - it can write only under `writable` (always the temporary directories and the Kex cache, which compiling and running its entrypoint need); - it can use the network only when `network?`; - it cannot read the places credentials are usually kept. Everything the child starts inherits the same limits. What this does NOT do, and must not be mistaken for: `Process.Run([...])` is not enforced — the child can start any program, though that program is held to the same limits — and `Net.Connect([...])` opens the network as a whole rather than to the listed hosts, because the sandbox filters by address and not by name. **Fields** - `writable` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `network?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) --- --- package: tey version: "0.2.1-dev" source: tey/self_update.kex title: Tey.SelfUpdate entities: - { kind: module, name: "Tey.SelfUpdate" } --- # Tey.SelfUpdate ## module `Tey.SelfUpdate` ### `currentPrefix` ```kex currentPrefix : String? ``` Where this Tey is installed, or None when it cannot be told — outside an installed launcher (a source checkout, `erl` by hand) there is nothing safe to overwrite. `tey/bin/tey` exports TEY_EBIN before invoking the BEAM, so a released install always knows. ### `installEbin` ```kex installEbin : String? ``` The launcher's own runtime directory. TEY_EBIN can instead name the Tey paired with the selected Kex (see `installPaired`), which is not an install to upgrade — TEY_INSTALL_EBIN always names the launcher's. ### `prefixOf` ```kex prefixOf(ebin: String) -> String? ``` TEY_EBIN with the trailing `/lib/kex/tey/ebin` stripped. Anything else — unset, empty, a checkout's `tey/ebin` — is None: there is no install prefix to upgrade in place. ### `homebrewManaged?` ```kex homebrewManaged?(prefix: String) -> Bool ``` Whether this install belongs to Homebrew, whose keg bookkeeping Tey must not fight: that Tey upgrades with `brew upgrade tey`. ### `manifestVersion` ```kex manifestVersion(text: String) -> String? ``` The `version("...")` line of a `package.kex` manifest, read as plain text. The manifest reader cannot be used here: it parses a LOCAL package through the selected compiler, and this text was just downloaded, not installed. ### `compareVersions` ```kex compareVersions(a: String, b: String) -> Ordering ``` Ordering for two version strings, for the already-newest guard. Something unparsable compares as older: a hand-edited development version must never read as "newer than every release" and refuse its own upgrade. Written over `newer?` rather than `compare` on purpose: a trait method from another module's `make ..., implement: Comparable` does not dispatch to a custom record from outside that module on the BEAM backend, while a plain `let` like `newer?` calls anywhere. ### `carryBakedErlText` ```kex carryBakedErlText(oldLauncher: String, newLauncher: String) -> String ``` A source install (`make -C tey install`) bakes an absolute `erl` into `bin/tey`; release archives bake a bare `erl` resolved from PATH. Upgrading must not silently move the former onto the latter, so the old absolute path is carried into the new launcher. Anything else (no line, a relative `erl`, the dev placeholder) leaves the new launcher exactly as published. ### `tagFor` ```kex tagFor(tags: [String], version: String) -> String? ``` The tag naming a Kex version, resolved against a tag list read once — `Tey.Toolchain.releaseTag` answers the same question but re-lists the tags on every call, which a pinned-version scan cannot afford. ### `firstSelfUpgrading` (constant) The oldest Tey with an `upgrade` command. Pinning an older one would strand the install — 0.2.0 cannot run `tey upgrade` to come back — so it is refused, with the installer as the deliberate way there. ### `installedVersion` ```kex installedVersion(prefix: String) -> String ``` The version of the Tey installed at `prefix`, which is not always the one running: with a Kex selected whose own Tey is unpacked beside it (see `installPaired`), the launcher runs that one instead. Comparing the running version against a release then answers "already the newest" while the install itself stays old. The installed launcher is asked directly, pointed at its own runtime; if that cannot be read, the running version is the answer — the behaviour before, and never a reason to downgrade. ### `upgradeLatest` ```kex upgradeLatest(includePre: Bool) -> Result ``` `tey upgrade` — the newest released Tey: the one the newest Kex release's manifest declares. Stable Kex releases unless `includePre` (`--pre`), which is how a pre-release Tey is reached without knowing its number. ### `upgradePinned` ```kex upgradePinned(version: String) -> Result ``` `tey upgrade ` — exactly that Tey, from the newest release (stable first, then pre-releases) whose manifest declares it. An older version is allowed: pinning is explicit, and it is the way back from a bad release. ### `installPaired` ```kex installPaired(kexVersion: String) -> Result ``` The Tey a Kex release ships, unpacked beside that toolchain so `bin/tey` runs it whenever that Kex is selected: Kex and Tey are developed and released as a pair, and a pre-release is only tested properly as that pair. Nothing installed is replaced — not even a Homebrew keg — which is what makes this safe to do on every `tey kex install`. The answer is the Tey version unpacked, or None when it was already there: tags do not move, so neither does what they carry. ## type `UpgradeOutcome` Upgrading Tey itself (`tey upgrade`), for installs that came from a release archive rather than a package manager — see `install.sh` at the repository root. A Homebrew-managed Tey is brew's to upgrade and is refused here. Tey has its own version and its own cadence (`tey/package.kex`), so it cannot reuse the Kex tag list directly: the newest Tey is the one the newest Kex release's manifest declares. That manifest is read over plain HTTPS (raw.githubusercontent.com) with the same curl-then-wget idiom `Tey.Toolchain.download` already uses — no GitHub API, no rate limits, no clone. The archive itself is the `tey-.tar.gz` the release workflow publishes beside the Kex archives, under the same tag. What `tey upgrade` found: either the version it installed and where, or why there was nothing to install. **Variants** - `Upgraded(String, String)` - `AlreadyNewest(String)` - `NewerThanRelease(String)` --- --- package: tey version: "0.2.1-dev" source: tey/semver.kex title: Tey.Semver entities: - { kind: module, name: "Tey.Semver" } --- # Tey.Semver ## module `Tey.Semver` ### `parseVersion` ```kex parseVersion(text: String) -> SemanticVersion? ``` ### `release` ```kex release(text: String) -> String ``` The release a version belongs to: `0.4.0-rc.1` → `0.4.0`. Used where a pre-release should count as its eventual release. ### `stable?` ```kex stable?(text: String) -> Bool ``` Whether a version is a full release rather than a pre-release. This is the question a download page asks: "what is the current stable Kex?". ### `satisfies` ```kex satisfies(versionText: String, requirement: String) -> Bool ``` ### `intersect` ```kex intersect(a: String, b: String) -> String ``` The requirement satisfied only by versions satisfying BOTH, for when two packages in one graph each ask for the same dependency. Concatenation, because `satisfies` already reads a requirement as a list of space-separated clauses that must ALL hold: `~> 0.1` and `>= 0.1.5` intersect to `~> 0.1 >= 0.1.5`, which is exactly the question "satisfies both". No interval arithmetic, no second implementation of the operators to keep in step with the first. An empty intersection is not detected here, and deliberately: `~> 0.1 ~> 0.2` is perfectly well formed and simply matches nothing. Emptiness is a fact about the versions a repository actually publishes, so it is discovered where the tags are (`highest`), where the error can also say what WAS available. ### `intersectAll` ```kex intersectAll(requirements: [String]) -> String ``` Every constraint at once. `[]` and `[""]` both mean "no opinion", which is what a package with no version requirement expresses. ### `versionOf` ```kex versionOf(ref: String) -> String ``` The version inside a resolved ref: `refs/tags/v0.1.1` and `v0.1.1` both give `0.1.1`. A ref that carries no version (a branch name, a bare commit) gives "", which satisfies nothing and so never claims to meet a range. ### `channelRank` ```kex channelRank(channel: String) -> Integer ``` Channel precedence, lowest first: prealpha < alpha < beta < rc < release. Plain semver would order these alphabetically — putting `beta` BEFORE `prealpha` — which is not what the words mean, so the ranking is explicit. ### `range?` ```kex range?(requested: String) -> Bool ``` Whether a requested tag is a RANGE rather than one exact tag. `v1.2.0` is a name to look up; `~> 1.2` is a question to answer against everything the repository has. ### `newer?` ```kex newer?(a: String, b: String) -> Bool ``` Descending semantic order, for picking the newest of anything. A tag that is not a version sorts after every tag that is, so a stray `nightly` can never displace a release. ### `newestFirst` ```kex newestFirst(versions: [String]) -> [String] ``` ### `highest` ```kex highest(tags: [String], requirement: String) -> String? ``` The highest tag that satisfies the requirement — the whole point of writing a range instead of a version. None when nothing does. ## record `SemanticVersion` **Fields** - `major` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `minor` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `patch` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `preRelease` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) Implements `Comparable`. ### Functions SemanticVersion is Comparable, so `a.compare(b)` answers Less/Equal/Greater like every other ordered type in the stdlib, and ordering reads as an ordering rather than as sign arithmetic on -1/0/1. #### `compare` ```kex compare(other: SemanticVersion) -> Ordering ``` --- --- package: tey version: "0.2.1-dev" source: tey/snapshot.kex title: Tey.Snapshot entities: - { kind: module, name: "Tey.Snapshot" } --- # Tey.Snapshot ## module `Tey.Snapshot` ### `create` ```kex create(name: String, declaredPath: String, declaringRoot: String) -> Result ``` Turns a path dependency into an immutable, content-addressed cache entry. The declared path remains in the lockfile for explanation; builds use the snapshot identified by `sha256`, never the live directory. ### `cachedPath` ```kex cachedPath(dependency: Tey.Lockfile.Dependency) -> String ``` ### `verify` ```kex verify(dependency: Tey.Lockfile.Dependency) -> Result ``` ### `sourceFiles` ```kex sourceFiles(root: String) -> [String] ``` The files a snapshot of `root` holds, relative to it: everything but VCS metadata, build output and `.teyignore` matches. ### `contentDigest` ```kex contentDigest(root: String) -> Result ``` The content digest a snapshot of `root` is published under. --- --- package: tey version: "0.2.1-dev" source: tey/toolchain.kex title: Tey.Toolchain entities: - { kind: module, name: "Tey.Toolchain" } --- # Tey.Toolchain ## module `Tey.Toolchain` ### `home` ```kex home : String ``` ### `repository` ```kex repository : String ``` Where `tey kex list` looks for releases. Overridable so a fork or a mirror can be used without patching Tey. ### `binDirectory` ```kex binDirectory : String ``` The directory holding the `kex` shim. It is what a user puts on PATH once; selecting a version repoints the shim rather than asking them to edit PATH again. ### `shimPath` ```kex shimPath : String ``` ### `selected` ```kex selected : String? ``` The version `tey kex use` last selected, if any. ### `installedVersions` ```kex installedVersions : [String] ``` Versions installed under this Tey home, newest first. Staging leftovers are not installations, and neither is a directory whose `bin/kex` is gone — a half-removed toolchain must not be offered as one somebody can run. ### `binaryOf` ```kex binaryOf(version: String) -> String ``` The `bin/kex` naming an installed version — the toolchain layout every consumer (the shim, `resolved`, the listings) agrees on, in one place. ### `installed?` ```kex installed?(version: String) -> Bool ``` ### `pairedTeyEbin` ```kex pairedTeyEbin(version: String) -> String ``` Where the Tey released with a Kex version lives — `bin/tey` reads the same path. Beside the toolchain rather than inside it: reinstalling a Kex replaces its whole directory, and a Tey running from there would lose its modules mid-run. ### `availableVersions` ```kex availableVersions : Result<[String], String> ``` Released versions, newest first, read from the repository's tags. Tags that are not versions are skipped rather than reported as junk. ### `newestFirst` ```kex newestFirst(versions: [String]) -> [String] ``` ### `latestStable` ```kex latestStable : Result ``` The newest RELEASED version — what `tey kex install` picks when asked for no version in particular, and what a download page means by "the current Kex". Pre-releases are opt-in by name. ### `latestIncludingPreReleases` ```kex latestIncludingPreReleases : Result ``` The newest version of ANY channel — what `tey kex install --pre` picks. This is how you get onto a beta or an rc without knowing its number: during a pre-stability run there may be nothing else published at all. ### `installedKex` ```kex installedKex : String? ``` ### `bundled` ```kex bundled : String? ``` The Kex bundled beside Tey — what a package manager lays down so the first `tey` command has a compiler without a ten-minute build. It is a SEED, not an installation: `kex` below adopts it into the Tey home on first use, and from then on Tey manages that copy like any other version. Nothing outside the Tey home is ever put on PATH, which is the whole point — two `kex` binaries under two managers drift apart. ### `bundledVersion` ```kex bundledVersion : String? ``` Which version the bundled Kex is. Answering means RUNNING it, so this is not something to ask inside a loop — but it is the only way to know: the seed is a plain install tree with no version recorded beside it. ### `selectableToolchains` ```kex selectableToolchains : [Toolchain] ``` Every version `tey kex use` can select, newest first: the ones in the Tey home, plus the bundled one — each carrying the binary that runs it, which is what a listing meant for tools (`kex list --installed --json`) needs. The seed belongs here even though it is not installed. It is a real compiler already on the disk, and on a fresh keg it is the one a bare `kex` runs — so leaving it out made `tey kex use ` refuse a version the user had just watched run, with no way back to it short of downloading a second copy of what was already there. ### `selectableVersions` ```kex selectableVersions : [String] ``` Every version `tey kex use` can select, newest first. ### `resolveSelector` ```kex resolveSelector(selector: String) -> String? ``` Turns what someone typed after `tey kex use` into a version number. `latest` means the newest Kex this machine can select — NOT the newest released one. `use` selects among what is already here; reaching for something unreleased-and-undownloaded is what `tey kex install` is for, and quietly downloading from a command that reads like "switch to" would be a surprise. ### `kex` ```kex kex : String ``` The compiler every Tey command shells out to. In order: an explicit TEY_KEX, the selected toolchain, the bundled seed (adopted on the spot), and only then a bare `kex` from PATH. That last one is a fallback for a source checkout, not a supported installation — a `kex` somebody else installed is a `kex` Tey cannot update, and it silently becomes a different version from the one `tey kex list` reports. ### `activeInfo` ```kex activeInfo : Result ``` Read the machine contract from the selected compiler. Keeping this in one call prevents dependency resolution and OTP validation from accidentally observing two different binaries if selection changes between them. ### `runtimeOtpFloor` ```kex runtimeOtpFloor : Integer? ``` The OTP release that compiled the selected toolchain's runtime beams — the oldest Erlang that can load them, since a `.beam` cannot be read by a runtime older than the `erlc` that produced it. Read from `kex --info` rather than guessed: the number is a build-time constant inside the binary (KEX_RUNTIME_OTP_FLOOR) and Tey has no other way to see it. `--version` is prose and may be reworded; `--info` is the contract (tests/info_cli_test.cxx pins it). `None` when the toolchain is too old to have `--info`, when the field is absent, or when it is `null` because the compiler was built without erlc. All three mean the same thing to a caller — "this toolchain does not say" — and none of them is an error worth stopping a build for. ### `runningOtpRelease` ```kex runningOtpRelease : Integer? ``` The OTP actually about to run, asked of `erl` itself. ### `erl` ```kex erl : String ``` The `erl` Tey and Kex will spawn. KEX_ERL mirrors what the compiler itself honours, so overriding it moves both rather than half of the pair. ### `resolved` ```kex resolved : String? ``` The compiler Tey would run, or None when there is none to run. The bundled Kex is used WHERE IT LIES rather than copied into the Tey home first. Copying is an install, and an install is not something to do behind someone's back on their first command — it is also impossible where it would be most useful, since Homebrew's post-install sandbox cannot write to the user's home at all. So a fresh Homebrew keg is runnable immediately, nothing is written until `tey kex install` is typed, and a selected version always wins over the bundled one. Without the last-resort fallback to a bare `kex` from PATH that `kex` above keeps: `kex` on PATH is Tey's own dispatcher, and answering it with its own name would send it round in a circle. ### `installBundled` ```kex installBundled : Result ``` Takes the bundled Kex into the Tey home so Tey manages it like any other version: `tey kex list` shows it, `tey kex use` can come back to it, and uninstalling whatever replaces it leaves something behind. Only ever done when asked — see `resolved`. ### `selectionHealthy?` ```kex selectionHealthy? : Bool ``` Whether the selected toolchain is actually there. A `current` naming a version somebody deleted leaves every command failing on a path that does not exist, which reads like a broken Tey rather than a missing Kex. ### `startupProblem` ```kex startupProblem(prefix: String) -> String? ``` Installs a released version by name. The published archive for this machine is the fast path — unpacking one takes seconds where building takes minutes, and it needs no compiler, which is the point: whatever installs Tey should not also have to be able to build Kex. Falling back to a source build rather than failing keeps the platforms without a published archive working, and covers a release whose upload is incomplete. Why an installed toolchain's `kex` cannot start on this machine, or None when it runs. A release archive is a prebuilt binary that loads readline, GMP, PCRE2 and OpenSSL from the system (and needs glibc 2.38+ on Linux); "installed" followed by a loader error on the first `kex` is the worst way to learn one of those is missing. ### `installRelease` ```kex installRelease(version: String) -> Result ``` ### `releaseBase` ```kex releaseBase : String ``` Where the release archives live. Derived from the repository so a fork only has to set TEY_KEX_REPO, and overridable on its own for a mirror. ### `releaseTarget` ```kex releaseTarget : Result ``` The archive naming this machine, matching what the release workflow builds: `kex-0.4.0-macos-arm64.tar.gz`. ### `installSource` ```kex installSource(source: String, version: String) -> Result ``` Installs compiler, runtime BEAM modules and stdlib sources together. The source tree's CMake install layout is the toolchain ABI Tey manages. ### `uninstall` ```kex uninstall(version: String) -> Result ``` Removes an installed toolchain. Refuses to leave the shim pointing at nothing: removing the SELECTED version also clears the selection, and the caller is told to pick another. ### `use` ```kex use(version: String) -> Bool ``` ### `download` ```kex download(url: String, path: String) -> Result ``` The archive download/verify/hash/tag/publish helpers below are public because `Tey.SelfUpdate` (`tey upgrade`) reuses the exact same release mechanics for Tey's own archives that Kex toolchains already trust — same URLs, same checksums, same staged swaps. No behavior change; only visibility. curl, then wget: one of the two is on essentially every machine that can reach a release, and this way Tey needs neither an HTTP library nor a compiler to fetch one. ### `verifyArchive` ```kex verifyArchive(archive: String, checksumUrl: String) -> Result ``` The published `.sha256` is ` `, the format both sha256sum and shasum write and read. ### `checksumOf` ```kex checksumOf(path: String) -> Result ``` Hashed with an external tool rather than read in: the archive is tens of megabytes of binary, and this keeps all of it out of the process. ### `releaseTag` ```kex releaseTag(version: String) -> Result ``` The tag naming this version in the repository — `0.3.0` or `v0.3.0`. ### `versionOf` ```kex versionOf(tag: String) -> String? ``` `v0.3.0` → Just("0.3.0"). A tag that is not a version is None, which `collect` drops: a toolchain is named by its version, not by a label. ### `publish` ```kex publish(staging: String, prefix: String) -> Result ``` Swaps a finished staging tree in for `prefix`. A reinstall keeps the old tree until the new one is in place and puts it back if the move fails, so a failed upgrade leaves the previous toolchain working rather than none. ## record `ToolchainInfo` Facts reported by the compiler Tey selected for this project. This is deliberately not Kex.VERSION: that names the compiler which built Tey itself, and a package may select a newer (or older) compiler to build its own sources. **Fields** - `version` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `reportedRuntimeOtpFloor` : [Integer](../../../prelude/0.4.0-rc.3-dev/number.md#make-integer)? (optional) ## record `Toolchain` One selectable Kex: the version `tey kex use` names it by, the binary that runs it, and whether it is the bundled seed lying beside Tey rather than an installation in the Tey home. **Fields** - `version` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `binary` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `bundled?` : [Bool](../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) (optional) ## type `ArchiveFailure` Why a published archive could not be installed. The distinction is the point: one of these is routine and one of them is not. **Variants** - `Unavailable(String)` - `Corrupt(String)` - `Unreachable(String)` --- --- package: tey version: "0.2.1-dev" source: tey/version.kex title: Tey.Version entities: - { kind: module, name: "Tey.Version" } --- # Tey.Version ## module `Tey.Version` GENERATED by tey/Makefile from tey/package.kex — do not edit. ### `number` (constant) ```kex number : String ``` Tey's own version, as declared by package.kex's version(...) line. --- --- package: tey version: "0.2.1-dev" source: tey/workspace.kex title: Tey.Workspace entities: - { kind: module, name: "Tey.Workspace" } --- # Tey.Workspace ## module `Tey.Workspace` ### `discover` ```kex discover(start: String) -> Result ``` Discovers the outermost workspace containing `start`. Without a workspace, the nearest package is a one-member context. Paths are lexical and absolute; discovery never changes the process working directory. ### `select` ```kex select(context: Context, packageName: String, workspace?: Bool) -> Result<[Member], String> ``` Selects the members a workspace-aware command should operate on. An explicit package always wins; otherwise commands at a member stay local, while commands at the workspace root (or another non-member directory) use every member. `--workspace` broadens the implicit member selection. ### `dependencyLayers` ```kex dependencyLayers(members: [Member]) -> [[Member]] ``` Dependency-ready layers for workspace-wide commands. Cyclic leftovers form one final layer so callers can continue, while discovery reports the cycle. ### `memberName` ```kex memberName(member: Member) -> String ``` ### `memberDependencies` ```kex memberDependencies(member: Member) -> [String] ``` ## record `Member` **Fields** - `memberRoot` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `memberManifest` : [ManifestPackage](../tey/manifest.md#record-tey-manifest-manifestpackage) ## record `Context` **Fields** - `workspaceRoot` : [String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `members` : [[Member](#record-tey-workspace-member)] (optional) - `currentMember` : [Member](#record-tey-workspace-member)? (optional) - `warnings` : [[String](../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/assets.kex title: Tey.Docgen.Assets entities: - { kind: module, name: "Tey.Docgen.Assets" } --- # Tey.Docgen.Assets Browser-side assets for the docs site: the version switcher and search. Both are progressive enhancements over static HTML — without JavaScript the badge stays a badge and search is absent. Each page carries its package and version in data- attributes, so the script fetches the right index without being regenerated per version. The script itself is assets/site.js, embedded at compile time. ## module `Tey.Docgen.Assets` ### `siteJs` ```kex siteJs : String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/cli.kex title: Tey.Docgen.Cli entities: - { kind: module, name: "Tey.Docgen.Cli" } --- # Tey.Docgen.Cli Command-line interface for docgen: option declarations, dispatch, usage. Inside a package (a directory with a package.kex), most of the line is already implied — `tey docs build --out ` is the whole command: ```kex cd tey && tey docs build --out ../kdocs tey docs build --source src/stdlib --out \ --package prelude --label "Prelude" --base-url https://docs.kex.run tey docs serve --out --port 4322 ``` A built-in Tey command. Everything after the command word is docgen's own vocabulary — OptionParser passes a command's options through — so no `--` is needed, though `tey docs -- build ...` works too (the `--` is stripped). ## module `Tey.Docgen.Cli` ### `options` ```kex options : OptionParser.OptionConfig ``` A function rather than a module-level constant: on BEAM, calling a make-method (`.parse`, `.help`) on a module-level constant fails with "Undefined function" — the dispatcher doesn't resolve the constant. The three subcommands are declared here, the way Tey declares its own, so `tey docs` help is generated from the same definitions that parse the line and reads like `tey help` does. ### `dispatch` ```kex dispatch(args: [String]) -> Integer ``` ### `strict` ```kex strict(parsed: OptionParser.ParsedOptions) -> Bool ``` After a command word, OptionParser hands an option it does not know on to the command as an argument, for commands with a vocabulary of their own. These three have none beyond the options above, so one left over is a typo — and `--sorce src` silently documenting the wrong directory is worse than an error. ### `printUsage` ```kex printUsage : Integer ``` The generated help, then what a build leaves behind — the one part of this command's story that no option or subcommand declares. ### `outputsHelp` ```kex outputsHelp : String ``` Styled like OptionParser's own blocks: a bold heading, then one aligned row per entry, padded on the unstyled name. --- --- package: tey version: "0.2.1-dev" source: tey/docgen/css.kex title: Tey.Docgen.Css entities: - { kind: module, name: "Tey.Docgen.Css" } --- # Tey.Docgen.Css The docs site stylesheet, ported by hand from kex-run-site's design tokens (src/styles/global.css): the warm honey/cream palette from the kex logo, Plus Jakarta Sans / Fraunces / JetBrains Mono, dark warm code surface with tok-* syntax classes. Plain CSS with custom properties — no Tailwind build, so the output stays host-anywhere static files. The stylesheet itself is assets/style.css, embedded at compile time so the build stays self-contained: Tey.Docgen.Index writes it to /assets/style.css. ## module `Tey.Docgen.Css` ### `siteCss` ```kex siteCss : String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/extract.kex title: Tey.Docgen.Extract entities: - { kind: module, name: "Tey.Docgen.Extract" } --- # Tey.Docgen.Extract Kex.AST nodes → the typed entity model (Tey.Docgen.Model). Declaration bodies interleave TypeAnnotations (`name : type`) with FunctionDefs (`let name(args) ...`); overloads repeat both. Functions are collected per name as PendingFunction records (annotations + parameter lists kept apart), then finalised once the body is fully walked, because pairing an annotation's curried type with a clause's parameter names needs both sides. ## module `Tey.Docgen.Extract` ### `fileEntities` ```kex fileEntities(items: [Kex.AST.Node]) -> [Entity] ``` ### `pageFor` ```kex pageFor(source: String, items: [Kex.AST.Node], header: String) -> SourcePage ``` `header` is the file's opening comment when a blank line separates it from what follows (Index.fileHeader). The AST reader attaches every comment above a declaration to it, blank lines and all, so the first declaration's doc arrives with the file's introduction glued on top — algebra.kex's "Algebraic structures: ..." read as the summary of `Monoid`. The header is cut off that doc here and kept as the page's own intro. ### `detachHeader` ```kex detachHeader(items: [Kex.AST.Node], header: String) -> [Kex.AST.Node] ``` The items with the header removed from the first documented one. ### `withoutHeader` ```kex withoutHeader(item: Kex.AST.Node, header: String) -> Kex.AST.Node ``` ### `docOf` ```kex docOf(item: Kex.AST.Node) -> String? ``` ### `withDoc` ```kex withDoc(item: Kex.AST.Node, doc: String?) -> Kex.AST.Node ``` ### `pageTitle` ```kex pageTitle(source: String, entities: [Entity]) -> String ``` A page's title is the declaration it is really about — "OptionParser", not the file name's "Optionparser", and "FileHandle", not "Filehandle". Preference order: a single top-level module, then a single top-level type/record/trait, then the file name title-cased. ### `baseSpelling` ```kex baseSpelling(name: String) -> String ``` "FileHandle" -> "FileHandle", "FS.Path" -> "Path": what the file could plausibly be named after. ### `moduleTitle` ```kex moduleTitle(entry: ModuleEntry, stem: String) -> String ``` The title of a file with one top-level module. The file's stem has a say when the module is shared across several files: data/queue.kex declares `module Data` around `module Queue`, and so do set.kex and stack.kex — without the stem, every one of those pages is titled "Data" and the sidebar shows indistinguishable entries. A module whose last segment matches the stem is the page ("dns.kex" -> Net.DNS); else a nested module matching it is ("queue.kex" -> Data.Queue); else the top module stands ("mock.kex" -> Mock, a file about the family). ### `lastSegmentMatches?` ```kex lastSegmentMatches?(qualifiedName: String, stem: String) -> Bool ``` ### `nestedModuleNamed` ```kex nestedModuleNamed(children: [Entity], stem: String) -> String? ``` ### `fileStem` ```kex fileStem(path: String) -> String ``` "kex/ast" -> "ast". ### `entitiesIn` ```kex entitiesIn(body: [Kex.AST.Node], prefix: String) -> [Entity] ``` ### `bodyEntities` ```kex bodyEntities(body: [Kex.AST.Node], prefix: String, accum: [Pending]) -> [Pending] ``` ### `finalize` ```kex finalize(accum: [Pending]) -> [Entity] ``` ### `mergeMakes` ```kex mergeMakes(entities: [Entity]) -> [Entity] ``` Two `make Queue` blocks in one file (one for the collection traits, one for Blankable) are one set of methods to a reader: the later block folds into the first, traits and members appended. ### `sameMakeTarget?` ```kex sameMakeTarget?(e: Entity, target: String) -> Bool ``` ### `absorbMake` ```kex absorbMake(e: Entity, later: MakeEntry) -> Entity ``` ### `finalizeOne` ```kex finalizeOne(p: Pending) -> Entity ``` ### `typeEntity` ```kex typeEntity(info: Kex.AST.TypeInfo, prefix: String) -> Entity ``` ### `variantEntry` ```kex variantEntry(v: Kex.AST.VariantInfo) -> VariantEntry ``` A named helper (rather than an inline lambda) so the element type of `v.fields` comes from VariantInfo, not from VariantEntry's [String]. ### `recordEntity` ```kex recordEntity(info: Kex.AST.RecordInfo, prefix: String) -> Entity ``` ### `traitEntity` ```kex traitEntity(info: Kex.AST.TraitInfo, prefix: String) -> Entity ``` ### `makeEntity` ```kex makeEntity(info: Kex.AST.MakeInfo) -> Entity ``` ### `moduleEntity` ```kex moduleEntity(info: Kex.AST.ModuleInfo, prefix: String) -> Entity ``` ### `constantEntity` ```kex constantEntity(info: Kex.AST.ConstantInfo, prefix: String) -> Entity ``` ### `functionEntries` ```kex functionEntries(body: [Kex.AST.Node], prefix: String, receiverMethods: Bool) -> [FunctionEntry] ``` ### `noteAnnotation` ```kex noteAnnotation(accum: [Pending], info: Kex.AST.AnnotationInfo, prefix: String) -> [Pending] ``` ### `addAnnotation` ```kex addAnnotation(p: Pending, info: Kex.AST.AnnotationInfo) -> Pending ``` ### `noteFunction` ```kex noteFunction(accum: [Pending], info: Kex.AST.FunctionInfo, prefix: String) -> [Pending] ``` ### `isConstantBinding` ```kex isConstantBinding(info: Kex.AST.FunctionInfo) -> Bool ``` A binding with no parameter list is a constant — unless it is `foul`, in which case it is a zero-argument FUNCTION that Kex auto-calls. `IO.out` is a handle that is always the same one; `IO.getLine` reads a new line every time it is named, and calling that a constant is a lie about both what it does and when it happens. ### `noteConstant` ```kex noteConstant(accum: [Pending], info: Kex.AST.FunctionInfo, prefix: String) -> [Pending] ``` ### `constantFromPending` ```kex constantFromPending(p: Pending, info: Kex.AST.FunctionInfo, prefix: String) -> Pending ``` The annotation arrived first and created a PendingFunction; the binding now replaces it with the constant entry, keeping name/doc/line. ### `returnTypeText` ```kex returnTypeText(info: Kex.AST.FunctionInfo) -> String ``` ### `addClause` ```kex addClause(p: Pending, info: Kex.AST.FunctionInfo, lists: [[ParamSig]]) -> Pending ``` ### `findPending` ```kex findPending(accum: [Pending], name: String) -> Integer? ``` ### `indexedFind` ```kex indexedFind(accum: [Pending], name: String, index: Integer) -> Integer? ``` ### `finalizeFunction` ```kex finalizeFunction(fn: PendingFunction) -> FunctionEntry ``` ### `declaredReturn` ```kex declaredReturn(fn: PendingFunction) -> String ``` The one return type the source commits to: the annotation's result, or the inline `-> T` of an unannotated clause. What the @return tag says is checked against this (Index.reportDocDrift), never shown instead of it. ### `finalResult` ```kex finalResult(ref: Kex.AST.TypeRef) -> Kex.AST.TypeRef ``` What a curried type finally answers: `A -> (A -> T -> A) -> A` is A. ### `docReturnType` ```kex docReturnType(doc: Doc) -> String ``` ### `functionTypes` ```kex functionTypes(fn: PendingFunction) -> [String] ``` The type names a function is about — every parameter type plus the result type of each annotation, deduplicated. This is what lets search answer "what touches Integer" rather than just "what is called Integer". ### `annotationTypes` ```kex annotationTypes(info: Kex.AST.AnnotationInfo, paramLists: [[ParamSig]]) -> [String] ``` ### `signatureTexts` ```kex signatureTexts(fn: PendingFunction) -> [String] ``` A signature names every parameter WITH its type and ends in the result: `and(a: Integer, b: Integer) -> Integer`. The names come from a clause, the types from the annotation (flattened to the clause's arity), from the clause's own inline annotations, or — for a method nobody annotated — from its @param/@return tags. A zero-parameter member reads as the property it looks like at the call site (`sum : Number`). A method with no clause at all (a trait's required method) has only its type, and borrows parameter names from its @param tags when they line up. ### `plainSignature` ```kex plainSignature(fn: PendingFunction, params: [ParamSig]) -> String ``` A signature built from the clause alone: inline types first, the doc tags as the fallback, bare names when neither says. ### `renderSignature` ```kex renderSignature(info: Kex.AST.AnnotationInfo, fn: PendingFunction) -> String ``` ### `docNamedSignature` ```kex docNamedSignature(name: String, ref: Kex.AST.TypeRef, doc: Doc) -> String ``` An annotation with no clause to take names from: the @param tags name the parameters when there are exactly as many as the type has. ### `paramText` ```kex paramText(name: String, typeName: String, hasDefault: Bool) -> String ``` ### `parenthesized` ```kex parenthesized(typeName: String) -> String ``` A function type inside a parameter list reads as the parameter's type only in parentheses: `f: (T -> Void)`, not `f: T -> Void`. ### `displayName` ```kex displayName(p: ParamSig, index: Integer, doc: Doc) -> String ``` A pattern parameter (`repeat(0)`) shows the name its @param tag gives that position, or `_` — never the literal, which reads as an argument. ### `docParamType` ```kex docParamType(doc: Doc, name: String) -> String ``` ### `bestParams` ```kex bestParams(lists: [[ParamSig]]) -> [ParamSig]? ``` Of several clauses' parameter lists, the one that names the most parameters — `repeat(n: Integer)` over `repeat(0)` — merged with the types the other clauses of the same arity declare. ### `namedCount` ```kex namedCount(list: [ParamSig]) -> Integer ``` ### `withPeerType` ```kex withPeerType(p: ParamSig, index: Integer, peers: [[ParamSig]]) -> ParamSig ``` ### `signatureTypeText` ```kex signatureTypeText(ref: Kex.AST.TypeRef) -> String ``` Source-style type text: function arrows chain to the right without the redundant parentheses typeRefText adds, and a function-typed parameter is parenthesized so `(X -> Y) -> Z` stays distinct from `X -> Y -> Z`. ### `signatureParamText` ```kex signatureParamText(p: Kex.AST.TypeRef) -> String ``` ### `annotationRef` ```kex annotationRef(info: Kex.AST.AnnotationInfo) -> Kex.AST.TypeRef ``` ### `matchingParamList` ```kex matchingParamList(ref: Kex.AST.TypeRef, paramLists: [[ParamSig]]) -> [ParamSig]? ``` ### `matchingParamLists` ```kex matchingParamLists(ref: Kex.AST.TypeRef, paramLists: [[ParamSig]]) -> [[ParamSig]] ``` ### `paramsMatch` ```kex paramsMatch(ref: Kex.AST.TypeRef, names: [ParamSig]) -> Bool ``` ### `flattenToArity` ```kex flattenToArity(ref: Kex.AST.TypeRef, arity: Integer) -> Kex.AST.TypeRef ``` Absorbs single-parameter curried layers while more than one argument remains. `(FilePath) -> (Read) -> R` with arity 2 becomes `(FilePath, Read) -> R`. ### `clauseParams` ```kex clauseParams(clause: Kex.AST.ClauseInfo) -> [ParamSig] ``` `type` is a keyword after `.`, so typed-record access goes through pattern matching instead. ### `paramSig` ```kex paramSig(p: Kex.AST.ParamInfo) -> ParamSig ``` ### `paramName` ```kex paramName(p: Kex.AST.ParamInfo) -> String ``` ### `annotationTypeOf` ```kex annotationTypeOf(info: Kex.AST.ConstantInfo) -> String ``` ### `fieldTypeName` ```kex fieldTypeName(f: Kex.AST.FieldInfo) -> String ``` ### `typeText` ```kex typeText(ref: Kex.AST.TypeRef) -> String ``` ### `typeTextOpt` ```kex typeTextOpt(ref: Kex.AST.TypeRef?) -> String ``` ### `qualify` ```kex qualify(prefix: String, name: String) -> String ``` Modules may be written fully qualified (`module FS.File do` inside `module FS do`), so a dotted name already carries its prefix — never prepend twice. A short name gets the parent's prefix as usual. ### `stripExt` ```kex stripExt(name: String) -> String ``` ### `titleCase` ```kex titleCase(s: String) -> String ``` ## type `Pending` Accumulator entries: finished entities or functions still collecting. **Variants** - `Done(Entity)` - `PendingFn(PendingFunction)` ## record `PendingFunction` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `annotations` : [[Kex.AST.AnnotationInfo](../../../../prelude/0.4.0-rc.3-dev/kex/ast.md#record-kex-ast-annotationinfo)] - `paramLists` : [[[ParamSig](#record-tey-docgen-extract-paramsig)]] - `clauseCount` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `doc` : [Doc](../../tey/docgen/model.md#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `returnType` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `ParamSig` One parameter as a clause declares it. `named` is false for a pattern parameter (`let repeat(0) = ...`), whose spelling is a value, not a name a signature should show. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `named` : [Bool](../../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) - `hasDefault` : [Bool](../../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/fragments.kex title: Tey.Docgen.Fragments entities: - { kind: module, name: "Tey.Docgen.Fragments" } --- # Tey.Docgen.Fragments `tey docs build --format fragments`: the reference as page bodies for a host site to frame, instead of a standalone site with docgen's own chrome. The unit lands where `site` output would (///), as ```kex .html the page body — no , no header, no sidebar, no h1 .page.json what the host needs to frame it: title, description, URL path, template, and `data` (package, version, source file, the on-page table of contents) index.html/.page.json the unit's landing page (the module cards) nav.json the unit's page tree: {title, path, children} *.md, model.json, search.json, manifest.json as in `site` output ``` Pages are addressed the way a static host with pretty URLs serves them: `list` is ///list/. Every relative link the shared renderer produced (`../net.html#module-net`) is rewritten to that form, so a fragment works wherever the host mounts the unit at ///. Nothing here knows which host that is. Marqraft reads this layout as a mount with "format": "fragments": each unit directory is a version, each sidecar a read-only page, nav.json the version's page tree. ## module `Tey.Docgen.Fragments` ### `write` ```kex write(pages: [SourcePage], versionDir: String, model: PackageModel) -> Integer ``` ### `writePage` ```kex writePage(page: SourcePage, versionDir: String, model: PackageModel) -> () ``` ### `unitData` ```kex unitData(model: PackageModel, source: String, toc: [TocEntry]) -> Any ``` Everything a theme might show beside the body: which unit and version this is (the header badge), where the source lives, and the page's sections for an "On this page" list. ### `tocValue` ```kex tocValue(t: TocEntry) -> Any ``` ### `tocHtml` ```kex tocHtml(toc: [TocEntry]) -> String ``` The rail as markup too, so a theme template can place it without walking JSON: a template language that sees the data as `Any` cannot iterate it. A page with a single section has nothing to navigate. ### `unitRoot` ```kex unitRoot(model: PackageModel) -> String ``` "/prelude/0.4.0/": where the unit is mounted. ### `pageUrl` ```kex pageUrl(model: PackageModel, urlPath: String) -> String ``` ### `navTree` ```kex navTree(pages: [SourcePage], model: PackageModel) -> Any ``` The page tree, nested the way the standalone sidebar nests it: a module family (Net, Net.DNS, Net.HTTP) is one branch. ### `navNode` ```kex navNode(page: SourcePage, pages: [SourcePage], model: PackageModel) -> Any ``` ### `tocOf` ```kex tocOf(entities: [Entity]) -> [TocEntry] ``` The sections a reader can jump to, in page order: each declaration with a section of its own, and those a module declares. ### `tocAt` ```kex tocAt(entities: [Entity], nested: Bool) -> [TocEntry] ``` ### `tocTitle` ```kex tocTitle(e: Entity) -> String ``` ### `descriptionOf` ```kex descriptionOf(page: SourcePage) -> String ``` One sentence for the page's : the file's intro, else what its first declaration says, else its title. ### `summaryOf` ```kex summaryOf(e: Entity) -> String ``` ### `absolutize` ```kex absolutize(html: String, model: PackageModel, urlPath: String) -> String ``` Every relative href in a fragment, rewritten to a site path: from the page at `urlPath`, "../net.html#module-net" is "/prelude/0.4.0/net/#module-net" and "index.html" is "/prelude/0.4.0/". A fragment-only href ("#x"), an absolute one and an external URL stay as they are. ### `sitePath` ```kex sitePath(href: String, base: [String]) -> String ``` ### `normalize` ```kex normalize(base: [String], rel: [String]) -> [String] ``` ## record `TocEntry` **Fields** - `title` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `kind` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `nested` : [Bool](../../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/highlight.kex title: Tey.Docgen.Highlight entities: - { kind: module, name: "Tey.Docgen.Highlight" } --- # Tey.Docgen.Highlight A small syntax highlighter for Kex source, ported from kex-run-site's src/lib/highlight.ts: a "good enough" tokenizer rather than a full grammar. Emits HTML at docgen build time, so the docs carry no client-side JavaScript. Runs over [Char] lists; pure recursion, no state. Fidelity note: backtick raw strings are not tracked inside interpolation braces, and `and`/`or` are colored as keywords even where the compiler treats them as identifiers — same trade-offs as the site version. ## module `Tey.Docgen.Highlight` ### `highlight` ```kex highlight(source: String) -> String ``` ### `lexAll` ```kex lexAll(cs: [Char], out: [Tok]) -> [Tok] ``` out accumulates newest-first; the final reverse restores source order. ### `nextToken` ```kex nextToken(cs: [Char], out: [Tok]) -> ([Tok], [Char]) ``` ### `lexString` ```kex lexString(cs: [Char], acc: [Tok]) -> ([Tok], [Char]) ``` cs starts after the opening quote; acc is newest-first tokens so far. ### `takeStringText` ```kex takeStringText(cs: [Char], acc: [Char]) -> (String, [Char]) ``` Collect literal string text up to (not including) the closing quote, an interpolation opener, or the end of input. Backslash escapes are opaque. ### `balancedBraces` ```kex balancedBraces(cs: [Char], depth: Integer, acc: [Char]) -> ([Char], [Char]) ``` From just after "${", collect the interpolation body up to its matching "}" (which is consumed but not collected). Embedded "..." strings are skipped whole so braces inside them do not count. ### `skipString` ```kex skipString(cs: [Char]) -> ([Char], [Char]) ``` Consume a whole "..." literal starting at the opening quote; returns the consumed characters (both quotes included) and the remainder. ### `skipStringBody` ```kex skipStringBody(cs: [Char], acc: [Char]) -> ([Char], [Char]) ``` ### `lexNumber` ```kex lexNumber(cs: [Char]) -> ([Tok], [Char]) ``` ### `exponentLength` ```kex exponentLength(cs: [Char]) -> Integer ``` ### `hexDigit?` ```kex hexDigit?(c: Char) -> Bool ``` ### `lexIdentifier` ```kex lexIdentifier(cs: [Char], out: [Tok]) -> ([Tok], [Char]) ``` ### `classifyWord` ```kex classifyWord(word: String, prevDot: Bool, nextCh: Char) -> String ``` ### `previousIsDot` ```kex previousIsDot(out: [Tok]) -> Bool ``` The most recent token that is not whitespace: out is newest-first, so a plain find walks backwards in time. ### `identChar?` ```kex identChar?(c: Char) -> Bool ``` ### `lexOperator` ```kex lexOperator(cs: [Char]) -> ([Tok], [Char]) ``` ### `twoCharOp` ```kex twoCharOp(cs: [Char]) -> String ``` ### `keywords` ```kex keywords : [String] ``` Functions rather than module constants: on BEAM, method calls on a module-level constant fail to dispatch. ### `constants` ```kex constants : [String] ``` ### `typeNames` ```kex typeNames : [String] ``` ### `renderTokens` ```kex renderTokens(toks: [Tok]) -> String ``` ### `highlightLinked` ```kex highlightLinked(source: String, hrefOf: (String -> String)) -> String ``` The same highlighting, with every type name that `hrefOf` can place wrapped in a link to it — signatures read as code and navigate as a reference. `hrefOf` answers "" for a name it cannot place. ### `renderLinkedTok` ```kex renderLinkedTok(t: Tok, hrefOf: (String -> String)) -> String ``` ### `renderTok` ```kex renderTok(t: Tok) -> String ``` ### `esc` ```kex esc(s: String) -> String ``` Local copy of Tey.Docgen.Html.esc — duplicating four replaces keeps the modules acyclic (Html calls Highlight for code blocks). ### `textOf` ```kex textOf(cs: [Char]) -> String ``` ### `startsWithChars` ```kex startsWithChars(cs: [Char], s: String) -> Bool ``` ## record `Tok` **Fields** - `cls` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `text` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/html.kex title: Tey.Docgen.Html entities: - { kind: module, name: "Tey.Docgen.Html" } --- # Tey.Docgen.Html Model records → static HTML pages, styled after kex.run's docs section (warm honey/cream palette, Fraunces/Plus Jakarta Sans/JetBrains Mono, dark code surface with tok-* syntax classes). The stylesheet lives in Tey.Docgen.Css and is written to assets/style.css by the build driver. HTML is a sibling renderer of the Markdown one: both walk the typed PackageModel, so nothing here parses Markdown or JSON. ## module `Tey.Docgen.Html` ### `pageHtml` ```kex pageHtml(page: SourcePage, model: PackageModel) -> String ``` ### `pageBodyHtml` ```kex pageBodyHtml(page: SourcePage, model: PackageModel, heading: Bool) -> String ``` A page's content without the chrome — what `pageHtml` wraps in the layout, and what a host site (the fragments output) wraps in its own. `heading` is false when the host renders the title itself. ### `submodulesHtml` ```kex submodulesHtml(page: SourcePage, model: PackageModel) -> String ``` A module family spread over several files ("Net" + net/dns.kex, net/http.kex, ...) gets one page per file, and the parent page says nothing about the rest — a reader on "Net" has no reason to believe Net.DNS or Net.Socket are documented at all. This section is the signpost: every descendant page, with the summary its module declares. ### `submoduleItem` ```kex submoduleItem(child: SourcePage, page: SourcePage, model: PackageModel) -> String ``` ### `preludePage?` ```kex preludePage?(page: SourcePage, model: PackageModel) -> Bool ``` ### `preludeBodyHtml` ```kex preludeBodyHtml(page: SourcePage, model: PackageModel, heading: Bool) -> String ``` The prelude declares nothing, so the generic page renderer leaves it blank. What it HAS is a list of imports and a comment explaining them — which is exactly the page a reader wants: what am I given for free, and where does each of those live. ### `preludeItem` ```kex preludeItem(model: PackageModel, fromPath: String, name: String) -> String ``` ### `preludeLink` ```kex preludeLink(model: PackageModel, fromPath: String, name: String) -> String ``` A `using X` in prelude.kex names a sibling SOURCE FILE, so the page for that file is the exact target. Going through the link index instead would lose half the list: `Map`, `Set` and `Range` are ambiguous as bare names — a module and a type both claim them — and resolve to nothing. The file is what matches, not the title: `using Test` is test.kex, whose page is titled "Assert", and `using Env` is env.kex, titled "ENV". ### `pageNamed` ```kex pageNamed(model: PackageModel, name: String) -> SourcePage? ``` ### `pageStem` ```kex pageStem(urlPath: String) -> String ``` ### `moduleSummary` ```kex moduleSummary(model: PackageModel, name: String) -> String ``` ### `versionIndexHtml` ```kex versionIndexHtml(model: PackageModel) -> String ``` Landing page for //index.html: every module as a card. When the package has a prelude, the cards are split by how a reader REACHES them — visible everywhere, or one `using` away — because that is the first question anyone has about a standard library. ### `versionIndexBodyHtml` ```kex versionIndexBodyHtml(model: PackageModel, heading: Bool) -> String ``` ### `cardModules` ```kex cardModules(model: PackageModel) -> [Tey.Docgen.Model.ModuleIndexEntry] ``` The modules worth a card. `Int8`, `Float32` and friends are modules holding two constants each, declared in number.kex beside Integer and Float: listing them beside `Net` and `FS` made the index read as if the stdlib had a dozen integer libraries. They are on their file's page. ### `constantsOnly?` ```kex constantsOnly?(model: PackageModel, m: Tey.Docgen.Model.ModuleIndexEntry) -> Bool ``` ### `indexSections` ```kex indexSections(model: PackageModel, top: [String]) -> String ``` ### `preludePaths` ```kex preludePaths(model: PackageModel) -> [String] ``` The prelude imports FILES, and everything a listed file declares comes with it: `using Time` is what puts `Date`, `Duration` and `Time.Period` in scope, none of which prelude.kex names. So the split is by page, not by module name. ### `preludePath` ```kex preludePath(model: PackageModel, name: String) -> [String] ``` ### `cardGrid` ```kex cardGrid(modules: [Tey.Docgen.Model.ModuleIndexEntry], top: [String]) -> String ``` ### `latestVersion` ```kex latestVersion(packageName: String, versions: [VersionEntry]) -> VersionEntry? ``` The version a reader gets by default: the highest release by semantic version, never an unreleased `-dev` build while a release exists. versions.json is in BUILD order, newest build first, and tools/build-docs.sh builds the working tree last, so its first entry is usually the -dev build rather than the latest release. ### `versionsNewestFirst` ```kex versionsNewestFirst(packageName: String, versions: [VersionEntry]) -> [VersionEntry] ``` One package's versions, highest first by semantic version. ### `packageRedirectHtml` ```kex packageRedirectHtml(packageName: String, versions: [VersionEntry]) -> String ``` /index.html: straight on to the latest version, so a link to a package lands on its reference rather than on a list of builds. Every other version stays one click away, at versions.html. ### `packageVersionsHtml` ```kex packageVersionsHtml(packageName: String, versions: [VersionEntry]) -> String ``` /versions.html: the versions of one package, newest first — the page a reader lands on when they want a different release of what they are reading. What the ids MEAN is the package's own story: a library's own releases, the stdlib's Kex releases. The page does not need to know. ### `versionCard` ```kex versionCard(v: VersionEntry) -> String ``` ### `searchPageHtml` ```kex searchPageHtml(model: PackageModel) -> String ``` //search.html: the shareable full-page search. The modal on every page links here with ?q=, so a results page is a URL rather than a state — the page reads the term back from the query string. ### `rootIndexHtml` ```kex rootIndexHtml(versions: [VersionEntry]) -> String ``` Site root index.html: one card per PACKAGE (its newest build), not per build — a reader arrives at "the docs" for a thing, and the thing is a package; its versions are one click down. Built from the accumulated versions.json entries, not just this build's model. ### `packageNames` ```kex packageNames(versions: [VersionEntry]) -> [String] ``` Package names in first-seen order of their newest entry: versions arrive newest-first from accumulateVersions, so the first entry of each package is that package's newest build. ### `packageCard` ```kex packageCard(name: String, versions: [VersionEntry]) -> String ``` ### `chrome` ```kex chrome(title: String, root: String, script: Bool, nav: String, content: String) -> String ``` What every full page shares: the , and the top bar with the brand on the left and `nav` on the right. `root` is the relative path back to the site root, and `script` says whether the page loads assets/site.js. ### `backLink` ```kex backLink(href: String, text: String) -> String ``` ### `layout` ```kex layout(title: String, current: String, bodyContent: String, model: PackageModel) -> String ``` ### `sidebar` ```kex sidebar(model: PackageModel, current: String) -> String ``` The sidebar lists one entry per source FILE, nested by module family: a page whose title is a dotted child of another page's title nests under it ("Net.DNS" under "Net", "Net.HTTP.WebSocket" under "Net.HTTP"), so a multi-file module reads as one branch instead of trailing the list in directory-walk order. The active file expands to its top-level declarations so a reader can jump straight to one. The label is the page's title (the module it declares), not the raw file name. The heading is a link: it is the package's name, and the obvious thing to want from it is the list of what the package contains — which is the version index, where the prelude split lives. ### `sidebarEntry` ```kex sidebarEntry(page: SourcePage, current: String, vpre: String, pages: [SourcePage]) -> String ``` ### `preludeTag` ```kex preludeTag(inPrelude: Bool) -> String ``` The page's own heading carries it — the sidebar is a list to scan, and a chip on 32 of 41 rows is noise rather than an answer. ### `firstModulesOfPages` ```kex firstModulesOfPages(pages: [SourcePage]) -> [String] ``` The modules that OPEN a page — their section sits at the very top, so a card for one links to the page itself rather than to an anchor that lands where the page already starts. A page whose first declaration is a type or record has no such module. ### `moduleCard` ```kex moduleCard(m: Tey.Docgen.Model.ModuleIndexEntry, top: Bool) -> String ``` ### `entitiesHtml` ```kex entitiesHtml(entities: [Entity], page: SourcePage, model: PackageModel) -> String ``` A page's declarations in source order — except that a type's `make` blocks on the same page are pulled into the type's own section, so `List` reads as one thing (the type, then its functions, grouped by the receiver they need) rather than a type followed by three loose "make" sections. ### `sectionEntities` ```kex sectionEntities(entities: [Entity]) -> [Entity] ``` The declarations that get a section of their own: everything except the make blocks folded into a type or trait on the same page. ### `declaredTypeNames` ```kex declaredTypeNames(entities: [Entity]) -> [String] ``` ### `declaredTraitNames` ```kex declaredTraitNames(entities: [Entity]) -> [String] ``` A `make ` beside its trait is the trait's defaults, rendered in the trait's own section (Relations.traitMethods). ### `makesFor` ```kex makesFor(entities: [Entity], name: String) -> [MakeEntry] ``` ### `entitySection` ```kex entitySection(e: Entity, page: SourcePage, model: PackageModel) -> String ``` ### `linker` ```kex linker(page: SourcePage, model: PackageModel) -> String -> String ``` A reference resolver scoped to one page: links resolve against the whole package index, but the href is relative to the page doing the referring. ### `hrefer` ```kex hrefer(page: SourcePage, model: PackageModel) -> String -> String ``` The bare href for a name, or "" — what linked highlighting and linked prose both need. ### `typeSection` ```kex typeSection(e: TypeEntry, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` ### `variantsHtml` ```kex variantsHtml(variants: [VariantEntry]?) -> String ``` Only real constructors: an abstract type (`type List`) has none, and a "Variants: (abstract)" heading told the reader nothing. ### `variantItem` ```kex variantItem(v: VariantEntry) -> String ``` ### `recordSection` ```kex recordSection(e: RecordEntry, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` ### `typeBodyHtml` ```kex typeBodyHtml(name: String, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` Everything about a type beyond its declaration: the traits it implements (from every file), its functions grouped by receiver, the functions it gets from those traits for free, and the other modules that add functions to it. ### `makeGroupHtml` ```kex makeGroupHtml(m: MakeEntry, base: String, page: SourcePage, model: PackageModel) -> String ``` "Functions" for the plain receiver, "On `[Number]`" for a specialised one. ### `genericReceiver?` ```kex genericReceiver?(target: String, base: String) -> Bool ``` `[X]` and `FileHandle` are the type itself; `[Number]` is not. ### `implementsHtml` ```kex implementsHtml(name: String, page: SourcePage, model: PackageModel) -> String ``` Every trait the type implements, wherever the implementing make block lives — algebra.kex is what makes Integer a Monoid, and number.kex's reader should still learn that. ### `traitLinks` ```kex traitLinks(traits: [String], page: SourcePage, model: PackageModel) -> String ``` ### `providedHtml` ```kex providedHtml(name: String, page: SourcePage, model: PackageModel) -> String ``` The trait functions a type gets without writing them: a trait's provided functions, minus any the type defines itself somewhere. This is the answer to "where does `List.eachIndexed` come from?". ### `providedFromHtml` ```kex providedFromHtml(written: String, own: [String], page: SourcePage, model: PackageModel) -> String ``` ### `elsewhereHtml` ```kex elsewhereHtml(name: String, page: SourcePage, model: PackageModel) -> String ``` Other modules that add functions to this type: `make Integer` in time.kex is where `5.seconds` comes from. ### `fieldRow` ```kex fieldRow(f: Tey.Docgen.Model.FieldEntry, link: (String -> String)) -> String ``` ### `linkTypeText` ```kex linkTypeText(link: (String -> String), text: String) -> String ``` A type text is code with links inside it: split into identifier and punctuation segments, link the identifiers that resolve, escape the rest. ### `linkType` ```kex linkType(links: [LinkEntry], fromPath: String, name: String) -> String ``` The whole-package resolution one segment goes through: an exact name hit links (display text escaped); anything else is plain escaped code. ### `traitSection` ```kex traitSection(e: TraitEntry, page: SourcePage, model: PackageModel) -> String ``` A trait: what an implementor must write, what it then gets for free, and who implements it. ### `implementedByHtml` ```kex implementedByHtml(e: TraitEntry, page: SourcePage, model: PackageModel) -> String ``` ### `makeSection` ```kex makeSection(e: MakeEntry, page: SourcePage, model: PackageModel) -> String ``` A make block with no type declaration on its page. On the type's home page (string.kex for String) it carries everything the type section would; elsewhere it points home. ### `firstMakeOnPage?` ```kex firstMakeOnPage?(facts: Tey.Docgen.Model.TypeFacts, e: MakeEntry, page: SourcePage) -> Bool ``` ### `homeNote` ```kex homeNote(base: String, facts: Tey.Docgen.Model.TypeFacts?, page: SourcePage, model: PackageModel) -> String ``` ### `moduleSection` ```kex moduleSection(e: ModuleEntry, page: SourcePage, model: PackageModel) -> String ``` A module's own functions and constants are its API, listed under it (h3) like a type's functions. Types, traits and nested modules it declares stand as sections of their own after that. ### `memberEntity?` ```kex memberEntity?(e: Entity) -> Bool ``` ### `moduleMemberHtml` ```kex moduleMemberHtml(e: Entity, page: SourcePage, model: PackageModel) -> String ``` ### `functionSection` ```kex functionSection(e: FunctionEntry, page: SourcePage, model: PackageModel) -> String ``` ### `constantSection` ```kex constantSection(e: ConstantEntry, page: SourcePage, model: PackageModel) -> String ``` ### `functionsHtml` ```kex functionsHtml(functions: [FunctionEntry], owner: String, level: Integer, implements: [String], page: SourcePage, model: PackageModel) -> String ``` Members of a trait or make block, one heading per NAME: overloads (`count` and `count(pred)`) are one function with several signatures, not two unrelated entries. `owner` scopes the anchors; `implements` lets a member that fulfils a trait function say which trait. ### `functionGroupHtml` ```kex functionGroupHtml(fs: [FunctionEntry], name: String, owner: String, level: Integer, implements: [String], page: SourcePage, model: PackageModel) -> String ``` ### `traitTag` ```kex traitTag(name: String, implements: [String], page: SourcePage, model: PackageModel) -> String ``` "from Enumerable": this member is the type's own version of a function the trait declares, linked to the trait's description of it. ### `overloadHtml` ```kex overloadHtml(f: FunctionEntry, page: SourcePage, model: PackageModel) -> String ``` One overload: its signature first (what you call), then the prose. ### `signaturesHtml` ```kex signaturesHtml(f: FunctionEntry, page: SourcePage, model: PackageModel) -> String ``` ### `docHtml` ```kex docHtml(doc: Doc, page: SourcePage, model: PackageModel) -> String ``` ### `entityDocHtml` ```kex entityDocHtml(doc: Doc, page: SourcePage, model: PackageModel) -> String ``` A declaration's doc: prose, then its examples and any deprecation — a type's @example is as much a part of it as a function's. ### `plainDocHtml` ```kex plainDocHtml(doc: Doc) -> String ``` Unlinked: for prose with no page to resolve against (the prelude comment, card summaries). ### `summaryParagraphs` ```kex summaryParagraphs(summary: String) -> [String] ``` Paragraphs break on blank lines — except inside a fenced block, where a blank line is part of the code rather than the end of a paragraph. ### `groupParagraphs` ```kex groupParagraphs(lines: [String], current: [String], accum: [String], fenced: Bool) -> [String] ``` ### `paragraphHtml` ```kex paragraphHtml(p: String, hrefOf: (String -> String)) -> String ``` A summary paragraph is prose, a fenced block that Rdoc produced from an indented (verbatim) block, or a bullet list — each rendered as what it is rather than reflowed into one paragraph. ### `bulletLine?` ```kex bulletLine?(line: String) -> Bool ``` ### `bulletListHtml` ```kex bulletListHtml(lines: [String], hrefOf: (String -> String)) -> String ``` `- item` lines, with indented continuation lines folded into the item above them. ### `inlineCode` ```kex inlineCode(text: String) -> String ``` Minimal inline Markdown for doc prose: `code` spans. Splitting on the backtick alternates plain/code segments; an unmatched trailing backtick degrades to literal text rather than swallowing the rest of the page. ### `linkedInline` ```kex linkedInline(text: String, hrefOf: (String -> String)) -> String ``` The same, with a code span that names something documented — a type, a trait, `List.map` — linked to it. ### `linkableCode?` ```kex linkableCode?(code: String) -> Bool ``` Only a bare name is a reference: `List`, `FS.Path`, `List.map`. A code span with spaces or parentheses is an expression, not a name. ### `firstSentence` ```kex firstSentence(text: String) -> String ``` ### `paramsHtml` ```kex paramsHtml(f: FunctionEntry, link: (String -> String), page: SourcePage, model: PackageModel) -> String ``` Each parameter's meaning. Its type is in the signature right above, so it is repeated here only when there is no signature to read it from. ### `returnsHtml` ```kex returnsHtml(f: FunctionEntry, link: (String -> String), page: SourcePage, model: PackageModel) -> String ``` The signature already states the result type, so when there is one "Returns" says only what the value MEANS. A @return tag whose type disagrees would otherwise put two answers side by side (`min : X?` above "Returns: Number?"). ### `examplesHtml` ```kex examplesHtml(doc: Doc) -> String ``` ### `deprecatedHtml` ```kex deprecatedHtml(doc: Doc) -> String ``` ### `esc` ```kex esc(s: String) -> String ``` Escape text for HTML (content and double-quoted attributes). ### `labelOf` ```kex labelOf(model: PackageModel) -> String ``` ### `pageTarget` ```kex pageTarget(current: String) -> String ``` The page the version switcher tries first in the target version. ### `markdownLink` ```kex markdownLink(current: String) -> String ``` Every page's raw Markdown, next to its HTML: the footer link humans see, and the address llms.txt tells models about. Index pages have none. The target is a bare sibling filename — the page already stands in the directory its `.md` was written to, so no prefix climbing is wanted. ### `slug` ```kex slug(name: String) -> String ``` ### `typeParamSuffix` ```kex typeParamSuffix(typeParams: [String]) -> String ``` ### `versionPrefix` ```kex versionPrefix(urlPath: String) -> String ``` Relative path from a page's urlPath back to the version directory: "string" → "" (sibling), "kex/ast" → "../". ### `rootPrefix` ```kex rootPrefix(urlPath: String) -> String ``` From the page to the site root: version dir is //. ### `ups` ```kex ups(n: Integer) -> String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/index.kex title: Tey.Docgen.Index entities: - { kind: module, name: "Tey.Docgen.Index" } --- # Tey.Docgen.Index The build driver: scan → extract → PackageModel, then emit everything derived from it — model.json, search.json, Markdown pages, manifest, versions, llms.txt/llms-full.txt, robots.txt and sitemap.xml. ## module `Tey.Docgen.Index` ### `buildCommand` ```kex buildCommand(parsed: OptionParser.ParsedOptions) -> Integer ``` ### `externalLinks` ```kex externalLinks(paths: String) -> [LinkEntry] ``` Names another unit documents, so this one's signatures and prose can link to them: the Tey reference links `String` and `FS.Path` into the standard library's. Each model.json says which unit it is; its pages sit at ../../// from this unit's version directory. A file that cannot be read is reported and skipped — links are an extra. ### `writeUnitIndexFiles` ```kex writeUnitIndexFiles(pages: [SourcePage], model: PackageModel, out: String) -> () ``` The unit's own index files — its manifest and the shared versions.json — without the site-level ones (landing pages, llms.txt, sitemap), which a host site that frames fragments owns. ### `reportDocDrift` ```kex reportDocDrift(pages: [SourcePage]) -> () ``` A @return tag that names a different type than the signature declares is a doc comment that has drifted from its code (`min : X?` documented as returning `Number?`). The page shows the signature's type; this says where the tag disagrees so the comment can be fixed. ### `normalizeType` ```kex normalizeType(text: String) -> String ``` ### `optString` ```kex optString(value: String?) -> String ``` ### `preludeInfo` ```kex preludeInfo(source: String) -> (String, [String], String) ``` A source tree with a `prelude.kex` has a subset that is visible without an import, and the reader wants to know which. The file is declarative by convention — an opening comment, then one bare `using ` per line — so reading it is enough. A package without one gets empties, and nothing in the output mentions a prelude at all. Returns (the file, the modules it makes visible, its opening comment). ### `leadingComment` ```kex leadingComment(lines: [String]) -> String ``` The file's opening comment block, `#` markers stripped, ready for the same Rdoc parse every other doc comment goes through. ### `commentBody` ```kex commentBody(line: String) -> String ``` ### `fileHeader` ```kex fileHeader(lines: [String]) -> String ``` The file's opening comment, when it is its own block: a blank line after it, not a declaration. A comment that runs straight into a declaration is that declaration's doc, and the page gets no intro. ### `usingName` ```kex usingName(line: String) -> String ``` "using List" -> "List"; anything else (a comment, a blank line, an `only:` list) -> "". ### `defaultSource` ```kex defaultSource(given: String) -> String ``` `src/` when it is standing there, else the flag as given (an empty one is reported by the scan: "no .kex files found in"). ### `defaultPackage` ```kex defaultPackage(given: String) -> String ``` The manifest's name when the CWD is a package — the identity docgen should not have to be told. Outside one, "": the caller has to say, and buildCommand refuses rather than inventing a name. Docgen used to answer "prelude" here, which meant a build with a swallowed or forgotten --package filed its pages under whatever the CWD's package.kex said (the stdlib was published as "kex" for exactly that reason, kexhq/kex#287). ### `defaultVersion` ```kex defaultVersion(given: String) -> String ``` The version of the thing being documented: a package's own release when standing in one. What a package's versions ARE differs per package — a library's own releases, the stdlib's Kex releases — so outside a package this is the caller's to state (--release) rather than docgen's to guess. ### `manifestVersion` ```kex manifestVersion : (String, String)? ``` (name, version) of the package.kex in the CWD, if there is a readable one. Both defaults read it; one read, one parse. ### `buildPages` ```kex buildPages(files: [String], sourceDir: String) -> [SourcePage] ``` ### `buildPagesLoop` ```kex buildPagesLoop(files: [String], sourceDir: String, pages: [SourcePage]) -> [SourcePage] ``` ### `writePages` ```kex writePages(pages: [SourcePage], versionDir: String, model: PackageModel) -> Integer ``` ### `writeHtml` ```kex writeHtml(pages: [SourcePage], versionDir: String, out: String, model: PackageModel) -> () ``` HTML mirrors the Markdown layout: one page per source file, plus the version landing page, the site-root redirect and the shared stylesheet. ### `dirNameOf` ```kex dirNameOf(urlPath: String) -> String ``` "kex/ast" → "kex"; "fs" → "" (already inside versionDir). ### `ensureDirectory` ```kex ensureDirectory(path: String) -> () ``` ### `writeIndexFiles` ```kex writeIndexFiles(pages: [SourcePage], model: PackageModel, out: String) -> () ``` ### `versionManifestJson` ```kex versionManifestJson(pages: [SourcePage], model: PackageModel) -> String ``` One version's pages, with the package and version they belong to, so a reader of the file needs nothing but the file. Page paths stay relative to the version directory — the URL prefix is `//`, and whoever assembles the site is the one that knows the site root. ### `manifestJson` ```kex manifestJson(pages: [SourcePage]) -> String ``` ### `accumulateVersions` ```kex accumulateVersions(out: String, model: PackageModel) -> [VersionEntry] ``` The output directory is the published site, so a build must not clobber the versions other builds published: read what is there, upsert this (package, version), write the union back, newest first. ### `upsertVersion` ```kex upsertVersion(current: VersionEntry, existing: [VersionEntry]) -> [VersionEntry] ``` This build's entry first, the other packages' and versions' kept: the pure half of accumulate, so the upsert rule itself can be spec'd. ### `readVersionEntries` ```kex readVersionEntries(out: String) -> [VersionEntry] ``` ### `parseVersionEntry` ```kex parseVersionEntry(v: Any) -> VersionEntry ``` ### `jsonText` ```kex jsonText(value: Any?) -> String ``` ### `labelOrPackage` ```kex labelOrPackage(model: PackageModel) -> String ``` ### `versionsJson` ```kex versionsJson(entries: [VersionEntry]) -> String ``` ### `versionEntryValue` ```kex versionEntryValue(e: VersionEntry) -> Any ``` ### `llmsTxt` ```kex llmsTxt(pages: [SourcePage], model: PackageModel) -> String ``` ### `llmsFullTxt` ```kex llmsFullTxt(pages: [SourcePage], model: PackageModel) -> String ``` ### `robotsTxt` ```kex robotsTxt(baseUrl: String) -> String ``` ### `sitemapXml` ```kex sitemapXml(pages: [SourcePage], model: PackageModel) -> String ``` ### `normalizeBaseUrl` ```kex normalizeBaseUrl(url: String) -> String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/json.kex title: Tey.Docgen.Json entities: - { kind: module, name: "Tey.Docgen.Json" } --- # Tey.Docgen.Json Model records → JSON artifacts. This is the single boundary where typed records become plain maps (atom keys, JSON-ready values) so JSON.stringify can serialise them: model.json (the full model) and search.json (a flat index for dynamic name/kind/type search on the docs site). ## module `Tey.Docgen.Json` ### `modelJson` ```kex modelJson(model: PackageModel) -> String ``` ### `moduleIndexValue` ```kex moduleIndexValue(m: Tey.Docgen.Model.ModuleIndexEntry) -> Any ``` ### `linkValue` ```kex linkValue(l: Tey.Docgen.Model.LinkEntry) -> Any ``` ### `labelOrPackage` ```kex labelOrPackage(model: PackageModel) -> String ``` ### `pageValue` ```kex pageValue(page: SourcePage) -> Any ``` ### `entityValue` ```kex entityValue(e: Entity) -> Any ``` ### `variantsValue` ```kex variantsValue(variants: [VariantEntry]?) -> Any ``` ### `fieldValue` ```kex fieldValue(f: FieldEntry) -> Any ``` ### `fieldValues` ```kex fieldValues(fields: [FieldEntry]) -> Any ``` A named helper so the lambda's element type comes from the [FieldEntry] parameter, not from the surrounding map literal's value type. ### `functionValue` ```kex functionValue(f: FunctionEntry) -> Any ``` ### `docValue` ```kex docValue(doc: Doc) -> Any ``` ### `returnsValue` ```kex returnsValue(doc: Doc) -> Any ``` ### `searchJson` ```kex searchJson(model: PackageModel) -> String ``` ### `searchEntries` ```kex searchEntries(model: PackageModel) -> [SearchEntry] ``` ### `pageSearchEntries` ```kex pageSearchEntries(page: SourcePage) -> [SearchEntry] ``` ### `entitySearchEntries` ```kex entitySearchEntries(e: Entity, page: SourcePage) -> [SearchEntry] ``` ### `entityTypes` ```kex entityTypes(e: Entity) -> [String] ``` The type names an entity is "about" — the search-by-type dimension. For a function that is its signature's types; for a record its field types; for a type its parents and variant fields; for a trait/make the types its methods mention; for a module the union of its children. ### `variantFieldTypes` ```kex variantFieldTypes(variants: [VariantEntry]?) -> [String] ``` ### `childSearchEntries` ```kex childSearchEntries(e: Entity, page: SourcePage) -> [SearchEntry] ``` ### `functionSearchEntry` ```kex functionSearchEntry(f: FunctionEntry, page: SourcePage, owner: String) -> SearchEntry ``` ### `searchValue` ```kex searchValue(entry: SearchEntry) -> Any ``` ### `entitySignatures` ```kex entitySignatures(e: Entity) -> [String] ``` ### `entitySummary` ```kex entitySummary(e: Entity) -> String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/markdown.kex title: Tey.Docgen.Markdown entities: - { kind: module, name: "Tey.Docgen.Markdown" } --- # Tey.Docgen.Markdown Model records → Markdown pages (LLM-friendly: stable headings, fenced kex signatures, frontmatter with the entity index). ## module `Tey.Docgen.Markdown` ### `pageMarkdown` ```kex pageMarkdown(page: SourcePage, model: PackageModel) -> String ``` ### `pageFrontmatter` ```kex pageFrontmatter(page: SourcePage, model: PackageModel) -> String ``` ### `entityFrontmatterLine` ```kex entityFrontmatterLine(e: Entity) -> String ``` ### `pageBody` ```kex pageBody(page: SourcePage, model: PackageModel) -> String ``` ### `entitiesMd` ```kex entitiesMd(entities: [Entity], page: SourcePage, model: PackageModel) -> String ``` The same layout as the HTML: a type's make blocks on its own page are folded into the type's section. ### `entitySection` ```kex entitySection(e: Entity, page: SourcePage, model: PackageModel) -> String ``` ### `linkMd` ```kex linkMd(model: PackageModel, fromPath: String, name: String) -> String ``` Cross-links in Markdown resolve against the same package index the HTML renderer uses — one resolution rule, two renderings. Targets are the .md siblings of this page. ### `hrefMd` ```kex hrefMd(fromPath: String, toPath: String, anchor: String) -> String ``` ### `linkTypeMd` ```kex linkTypeMd(model: PackageModel, fromPath: String, text: String) -> String ``` ### `typeSection` ```kex typeSection(e: TypeEntry, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` ### `variantBullet` ```kex variantBullet(name: String, fields: [String]) -> String ``` ### `recordSection` ```kex recordSection(e: RecordEntry, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` ### `typeBodyMd` ```kex typeBodyMd(name: String, makes: [MakeEntry], page: SourcePage, model: PackageModel) -> String ``` ### `implementsMd` ```kex implementsMd(name: String, page: SourcePage, model: PackageModel) -> String ``` ### `traitLinksMd` ```kex traitLinksMd(traits: [String], page: SourcePage, model: PackageModel) -> String ``` ### `providedMd` ```kex providedMd(name: String, page: SourcePage, model: PackageModel) -> String ``` ### `providedFromMd` ```kex providedFromMd(written: String, own: [String], page: SourcePage, model: PackageModel) -> String ``` ### `elsewhereMd` ```kex elsewhereMd(name: String, page: SourcePage, model: PackageModel) -> String ``` ### `traitSection` ```kex traitSection(e: TraitEntry, page: SourcePage, model: PackageModel) -> String ``` ### `makeSection` ```kex makeSection(e: MakeEntry, page: SourcePage, model: PackageModel) -> String ``` ### `moduleSection` ```kex moduleSection(e: ModuleEntry, page: SourcePage, model: PackageModel) -> String ``` ### `constantSection` ```kex constantSection(e: ConstantEntry) -> String ``` ### `functionsMd` ```kex functionsMd(functions: [FunctionEntry], owner: String, level: Integer, implements: [String], page: SourcePage, model: PackageModel) -> String ``` One heading per name; overloads listed under it. ### `traitTagsMd` ```kex traitTagsMd(name: String, implements: [String], page: SourcePage, model: PackageModel) -> String ``` ### `overloadMd` ```kex overloadMd(f: FunctionEntry, page: SourcePage, model: PackageModel) -> String ``` ### `signatureParts` ```kex signatureParts(parts: [String], f: FunctionEntry) -> [String] ``` ### `paramParts` ```kex paramParts(parts: [String], f: FunctionEntry) -> [String] ``` ### `returnParts` ```kex returnParts(parts: [String], f: FunctionEntry) -> [String] ``` ### `returnLine` ```kex returnLine(r: Return) -> String ``` ### `exampleParts` ```kex exampleParts(parts: [String], doc: Doc) -> [String] ``` ### `exampleBlocks` ```kex exampleBlocks(examples: [Example]) -> [String] ``` ### `exampleBlock` ```kex exampleBlock(ex: Example) -> [String] ``` ### `entityDocMd` ```kex entityDocMd(doc: Doc) -> String ``` ### `docBlock` ```kex docBlock(doc: Doc) -> String ``` ### `typeParamSuffix` ```kex typeParamSuffix(typeParams: [String]) -> String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/md.kex title: Tey.Docgen.Md entities: - { kind: module, name: "Tey.Docgen.Md" } --- # Tey.Docgen.Md Markdown → typed blocks → HTML. Docgen already had a Markdown *emitter* (Tey.Docgen.Markdown, model → md for LLMs) and an RDoc parser for doc comments; neither reads a Markdown document. This does, so hand-written prose can go through the same chrome as the generated reference. The subset is what the kex.run guide pages actually use: YAML-ish frontmatter, ATX headings, paragraphs, fenced code (```kex runs through the highlighter), blockquotes, ordered and unordered lists with nesting, GFM tables, thematic breaks, and inline code / bold / italics / links. It is deliberately not CommonMark — there is no HTML passthrough, no reference links, no setext headings — and anything it does not recognise stays a paragraph rather than becoming an error. Nothing here knows about guides, books, or the kexhq site. ## module `Tey.Docgen.Md` ### `parseDocument` ```kex parseDocument(text: String) -> Document ``` The whole job: text in, document out. ### `toHtml` ```kex toHtml(text: String) -> String ``` Text in, HTML out — `parse` then `render`, which is all a caller wanting a page body needs. ### `splitFrontmatter` ```kex splitFrontmatter(lines: [String]) -> (Frontmatter, [String]) ``` --- frontmatter ------------------------------------------------------- A leading `---` fence delimits frontmatter. Without one the document is all body, and every field takes its default. ### `closingFence` ```kex closingFence(lines: [String], index: Integer) -> Integer? ``` ### `frontmatterOf` ```kex frontmatterOf(lines: [String], accum: Frontmatter) -> Frontmatter ``` ### `assign` ```kex assign(front: Frontmatter, key: String, value: String) -> Frontmatter ``` ### `unquote` ```kex unquote(value: String) -> String ``` ### `trimEnds` ```kex trimEnds(value: String) -> String ``` Drops the first and last character — the quotes `quoted?` just matched. ### `quoted?` ```kex quoted?(value: String, mark: String) -> Bool ``` ### `parseBlocks` ```kex parseBlocks(lines: [String]) -> [MdBlock] ``` --- block structure --------------------------------------------------- ### `blockLoop` ```kex blockLoop(lines: [String], accum: [MdBlock]) -> [MdBlock] ``` One pass over the lines, each branch consuming the whole construct it recognises and returning the lines it did not take. ### `fence?` ```kex fence?(line: String) -> Bool ``` ### `rule?` ```kex rule?(line: String) -> Bool ``` `---`, `***` or `___`, three or more, nothing else on the line. Checked after frontmatter is gone, so a stray `---` here really is a break. ### `ruleOf?` ```kex ruleOf?(text: String, mark: String) -> Bool ``` ### `headingLevel` ```kex headingLevel(line: String) -> Integer ``` `##` → 2. Zero when the line is not a heading; `#hashtag` is not one. ### `countHashes` ```kex countHashes(text: String, seen: Integer) -> Integer ``` ### `takeCode` ```kex takeCode(lines: [String]) -> (MdBlock, [String]) ``` --- fenced code ------------------------------------------------------- The info string is the language. An unterminated fence runs to the end of the document rather than failing — a truncated page still renders. ### `closingCodeFence` ```kex closingCodeFence(lines: [String], index: Integer) -> Integer ``` ### `takeQuote` ```kex takeQuote(lines: [String]) -> (MdBlock, [String]) ``` --- blockquotes ------------------------------------------------------- `>` is stripped from each line and what remains is parsed as blocks, so a quote can hold anything a document can, nesting included. ### `quoteLine?` ```kex quoteLine?(line: String) -> Bool ``` ### `stripQuote` ```kex stripQuote(line: String) -> String ``` ### `takeList` ```kex takeList(lines: [String]) -> (MdBlock, [String]) ``` --- lists ------------------------------------------------------------- Nesting is by indentation: a line indented past the first item's marker belongs to that item and is parsed as blocks in its own right, so a list can hold paragraphs, code and deeper lists. ### `listLine?` ```kex listLine?(line: String) -> Bool ``` A list continues through its own items, indented continuation lines, and blank lines — but a blank line followed by unindented text ends it, which is why the run is cut at the first line that is neither. ### `listItems` ```kex listItems(lines: [String], indent: Integer, current: [String], accum: [ListItem]) -> [ListItem] ``` ### `flushItem` ```kex flushItem(current: [String], accum: [ListItem]) -> [ListItem] ``` The first line is the item's own text; anything below it is nested content, dedented so it parses as a document of its own. ### `dropTrailingBlanks` ```kex dropTrailingBlanks(lines: [String]) -> [String] ``` ### `dedent` ```kex dedent(lines: [String]) -> [String] ``` Removes the common indent, so nested content is parsed at column zero. ### `indentOf` ```kex indentOf(line: String) -> Integer ``` ### `itemMarker` ```kex itemMarker(line: String) -> String ``` ### `bulletMarker` ```kex bulletMarker(line: String) -> String ``` ### `numberMarker` ```kex numberMarker(line: String) -> String ``` "12. " → "12." ; anything else → "". ### `tableStart?` ```kex tableStart?(lines: [String]) -> Bool ``` --- tables ------------------------------------------------------------ A GFM table is a header row and a delimiter row of dashes; without the delimiter the lines are ordinary paragraphs. ### `delimiterRow?` ```kex delimiterRow?(line: String) -> Bool ``` ### `delimiterChar?` ```kex delimiterChar?(c: String) -> Bool ``` ### `takeTable` ```kex takeTable(lines: [String]) -> (MdBlock, [String]) ``` ### `cellsOf` ```kex cellsOf(line: String) -> [String] ``` Leading and trailing pipes are optional, so the split is trimmed of the empty cells they produce. ### `takeParagraph` ```kex takeParagraph(lines: [String]) -> (MdBlock, [String]) ``` --- paragraphs -------------------------------------------------------- Runs to a blank line or to the start of any other construct, so a list or fence directly under prose is not swallowed into it. ### `paragraphLine?` ```kex paragraphLine?(line: String) -> Bool ``` ### `render` ```kex render(items: [MdBlock]) -> String ``` --- rendering --------------------------------------------------------- ### `renderBlock` ```kex renderBlock(block: MdBlock) -> String ``` ### `renderCode` ```kex renderCode(language: String, body: String) -> String ``` Kex code goes through the same highlighter the reference pages use, so a snippet in the guide looks like a signature in the reference. Everything else is escaped and left plain. ### `renderItems` ```kex renderItems(items: [ListItem]) -> String ``` ### `renderItem` ```kex renderItem(item: ListItem) -> String ``` ### `renderTable` ```kex renderTable(header: [String], rows: [[String]]) -> String ``` ### `renderRow` ```kex renderRow(row: [String]) -> String ``` ### `anchor` ```kex anchor(text: String) -> String ``` A heading's id: lowercase, non-alphanumerics folded to single dashes. ### `trimDashes` ```kex trimDashes(text: String) -> String ``` `String.trim` takes no argument, and the folding above can leave a dash at either end (`## \`try\`` → `-try-`). ### `anchorChar` ```kex anchorChar(c: Char) -> String ``` ### `squeezeDashes` ```kex squeezeDashes(text: String, accum: String) -> String ``` ### `stripInline` ```kex stripInline(text: String) -> String ``` Inline markers removed, for anchors and any other plain-text use. ### `inline` ```kex inline(text: String) -> String ``` --- inline ------------------------------------------------------------ Code spans first and separately: their content is escaped but never scanned for emphasis, so `a*b*c` inside backticks stays literal. ### `inlineLoop` ```kex inlineLoop(text: String, accum: String) -> String ``` ### `emphasis` ```kex emphasis(text: String) -> String ``` Links before emphasis, so a `*` inside a URL is not read as a marker. ### `links` ```kex links(text: String, accum: String) -> String ``` ### `linkParts` ```kex linkParts(rest: String) -> (String, String, String)? ``` `label](href)` → (label, href, what follows). None when the shape is not a link after all, in which case the `[` is literal text. ### `marks` ```kex marks(text: String) -> String ``` `**bold**` before `*italic*`, or the opening `**` would parse as an empty emphasis. Escaping happens here, on the text that is left. ### `wrap` ```kex wrap(text: String, strongMark: String, strongTag: String, emMark: String, emTag: String) -> String ``` ### `pairs` ```kex pairs(text: String, mark: String, tag: String, accum: String) -> String ``` Replaces each surviving pair of markers with a tag. An unpaired marker is left as written rather than swallowing the rest of the line. ### `indexOfText` ```kex indexOfText(haystack: String, needle: String, at: Integer) -> Integer? ``` Where `needle` first occurs in `haystack`, or None. String.indexOf takes a Char, and the markers here are two characters wide ("**"), so the search is spelled out. ### `esc` ```kex esc(s: String) -> String ``` ## record `Frontmatter` A document's frontmatter: the keys the prose pipeline reads. Unknown keys are parsed and ignored rather than rejected, so a page can carry metadata a later renderer will use. **Fields** - `title` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `description` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `order` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) (optional) ## record `ListItem` One item of a list: its own inline text, plus any blocks nested under it. **Fields** - `text` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `children` : [[MdBlock](#type-tey-docgen-md-mdblock)] ## type `MdBlock` **Variants** - `Heading(Integer, String)` - `Paragraph(String)` - `Code(String, String)` - `Quote([MdBlock])` - `Bullets([ListItem])` - `Numbers([ListItem])` - `Table([String], [[String]])` - `Rule` ## record `Document` **Fields** - `frontmatter` : [Frontmatter](#record-tey-docgen-md-frontmatter) - `blocks` : [[MdBlock](#type-tey-docgen-md-mdblock)] --- --- package: tey version: "0.2.1-dev" source: tey/docgen/model.kex title: Tey.Docgen.Model entities: - { kind: module, name: "Tey.Docgen.Model" } --- # Tey.Docgen.Model The canonical documentation model. Extraction produces these records; Markdown, JSON and the search index are all derived from them. Field names avoid `type` (a keyword after `.`), so every field is dot-accessible; JSON keys are chosen at the emission boundary in Tey.Docgen.Json. ## module `Tey.Docgen.Model` ### `emptyDoc` ```kex emptyDoc : Doc ``` ### `kindOf` ```kex kindOf(entity: Entity) -> String ``` The kind label used in indexes and JSON for each entity variant. ### `nameOf` ```kex nameOf(entity: Entity) -> String ``` The primary display name of an entity (a make's is its target type). ### `qualifiedNameOf` ```kex qualifiedNameOf(entity: Entity) -> String ``` ### `lineOf` ```kex lineOf(entity: Entity) -> Integer ``` ### `anchorOf` ```kex anchorOf(entity: Entity) -> String ``` The anchor id an entity's HTML section renders at. Lives here — not in the HTML renderer — because the cross-link index and the sidebar compute the same ids, and the HTML must match whatever they resolved to. ### `anchorNameOf` ```kex anchorNameOf(entity: Entity) -> String ``` A make's anchor is keyed by its full target spelling (type params included), a module's by its qualified name — the section ids are derived from the same texts. ### `memberAnchorOf` ```kex memberAnchorOf(owner: String, name: String) -> String ``` The anchor of a function documented inside a trait or make section: scoped by its owner, because `count` is a member of both `[X]` and `String`, and one page can hold several owners. ### `ownerSlugOf` ```kex ownerSlugOf(owner: String) -> String ``` An owner spelled as a type: `[X]` -> "list", `[Number]` -> "list-number", `FileHandle` -> "filehandle-canread". Type variables (a lone capital, optionally numbered) say nothing about WHICH methods these are, so they are dropped rather than baked into every anchor. ### `typeVariable?` ```kex typeVariable?(word: String) -> Bool ``` ### `wordsOf` ```kex wordsOf(text: String) -> [String] ``` Identifier-ish runs of a type text: letters, digits, `_`, `?`, `!`. ### `wordSplit` ```kex wordSplit(rest: String, word: String, out: [String]) -> [String] ``` ### `slugOf` ```kex slugOf(name: String) -> String ``` An id-safe spelling of a name: lower case, every run of punctuation one dash. `?` and `!` survive — they are part of Kex names (`empty?`) and legal in a fragment. ### `operatorSlug` ```kex operatorSlug(name: String) -> String ``` An operator method has no letters to slug: `+` is "op-plus", `<=>` is "op-lt-eq-gt" — distinct, stable and readable in a URL. ### `operatorCharName` ```kex operatorCharName(c: Char) -> String ``` ### `baseTypeOf` ```kex baseTypeOf(target: String) -> String ``` "[X]" -> "List", "{K: V}" -> "Map", "FileHandle" -> "FileHandle", "Map" -> "Map": the type a make block extends, whatever specialisation it spells. ## record `Param` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `description` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `Return` **Fields** - `typeName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `description` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `Example` **Fields** - `caption` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `code` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `Doc` **Fields** - `summary` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `params` : [[Param](#record-tey-docgen-model-param)] - `returns` : [Return](#record-tey-docgen-model-return)? - `examples` : [[Example](#record-tey-docgen-model-example)] - `deprecated` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)? ## record `VariantEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `fields` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] ## record `FieldEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `hasDefault` : [Bool](../../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) ## record `TypeEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeParams` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `parents` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `variants` : [[VariantEntry](#record-tey-docgen-model-variantentry)]? - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `RecordEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeParams` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `fields` : [[FieldEntry](#record-tey-docgen-model-fieldentry)] - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `TraitEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeParams` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `functions` : [[FunctionEntry](#record-tey-docgen-model-functionentry)] - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `MakeEntry` **Fields** - `target` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `isFinal` : [Bool](../../../../prelude/0.4.0-rc.3-dev/truthyable.md#make-bool) - `implements` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `functions` : [[FunctionEntry](#record-tey-docgen-model-functionentry)] - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `ModuleEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `children` : [[Entity](#type-tey-docgen-model-entity)] - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `FunctionEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `signatures` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `clauseCount` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `types` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `returnType` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `ConstantEntry` **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `typeName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `doc` : [Doc](#record-tey-docgen-model-doc) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## type `Entity` **Variants** - `TypeDecl(TypeEntry)` - `RecordDecl(RecordEntry)` - `TraitDecl(TraitEntry)` - `MakeDecl(MakeEntry)` - `ModuleDecl(ModuleEntry)` - `FuncDecl(FunctionEntry)` - `ConstDecl(ConstantEntry)` ## record `SourcePage` **Fields** - `source` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `title` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `entities` : [[Entity](#type-tey-docgen-model-entity)] - `intro` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) ## record `Extension` One `make` block seen from the type it extends: where it is, what it adds, and which traits it declares. A type's methods are spread over many files (`make Integer` lives in number.kex, time.kex, units.kex and algebra.kex), and these rows are what put them back together. **Fields** - `target` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `pageTitle` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `implements` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `names` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] ## record `TypeFacts` Everything the package says about one type, across files. `homePath` is the page a reader should land on for the type — the one declaring it, or the one named after it for builtins such as Integer and String. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `homePath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `homeAnchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `extensions` : [[Extension](#record-tey-docgen-model-extension)] - `traits` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] ## record `TraitFacts` A trait seen from the outside: where it is documented and which make blocks implement it. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `implementors` : [[Extension](#record-tey-docgen-model-extension)] ## record `ModuleIndexEntry` The navigation unit: one per module in the package, pointing at the page its entities live on. The sidebar and the module index of model.json are built from these — a FILE is where source lives, a MODULE is what a reader navigates by, and the two only coincide by accident. **Fields** - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `summary` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `members` : [[MemberEntry](#record-tey-docgen-model-memberentry)] ## record `MemberEntry` One documented member of a module, with the anchor its section renders at — the sidebar's expanded list and the JSON index both use these. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `LinkEntry` One row of the cross-link index: a name a type reference elsewhere in the package may mention ("String", "FS.Path") and where it lands. The index is what makes interlinked docs possible without per-renderer guesswork: every renderer resolves names against the same list. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `kind` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) ## record `PackageModel` **Fields** - `package` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `version` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `label` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `baseUrl` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `generatedAt` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `pages` : [[SourcePage](#record-tey-docgen-model-sourcepage)] - `modules` : [[ModuleIndexEntry](#record-tey-docgen-model-moduleindexentry)] (optional) - `links` : [[LinkEntry](#record-tey-docgen-model-linkentry)] (optional) - `preludeModules` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `preludeSource` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `preludeDoc` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `types` : [[TypeFacts](#record-tey-docgen-model-typefacts)] (optional) - `traits` : [[TraitFacts](#record-tey-docgen-model-traitfacts)] (optional) ## record `SearchEntry` A flattened search row: one per entity (and per function inside traits, makes and modules), joining name/kind/signature to the page URL. The `types` are the type names the entity is about — the types its signature mentions — so search can answer by name AND by type; `anchor` is the section id a result deep-links to. **Fields** - `name` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `qualifiedName` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `kind` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `signatures` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] - `types` : [[String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)] (optional) - `summary` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `urlPath` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `anchor` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) (optional) - `line` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) ## record `VersionEntry` One row of versions.json. The file accumulates across builds — the output directory IS the site, so each build upserts its (package, id) entry into whatever is already published there. **Fields** - `id` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `label` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `generatedAt` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `package` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/nav.kex title: Tey.Docgen.Nav entities: - { kind: module, name: "Tey.Docgen.Nav" } --- # Tey.Docgen.Nav Navigation and cross-linking: the two indexes every renderer resolves against, derived once from the extracted pages. ```kex modules — one ModuleIndexEntry per module (the sidebar's structure) links — one LinkEntry per linkable name (the interlink universe) ``` Both are computed here rather than in each renderer so HTML, Markdown and model.json cannot disagree about where a name lands — the same reasoning that keeps the typed model the single source: resolve once, render often. ## module `Tey.Docgen.Nav` ### `buildModules` ```kex buildModules(pages: [SourcePage]) -> [ModuleIndexEntry] ``` Every module in the package, in declaration order, pointing at the page it lives on. Nested modules are entries of their own (the sidebar nests them by name). ### `modulesOnPage` ```kex modulesOnPage(entities: [Entity], urlPath: String) -> [ModuleIndexEntry] ``` ### `moduleEntry` ```kex moduleEntry(entry: ModuleEntry, urlPath: String) -> ModuleIndexEntry ``` ### `firstLine` ```kex firstLine(summary: String) -> String ``` ### `parentTitleOf` ```kex parentTitleOf(pages: [SourcePage], page: SourcePage) -> String? ``` The file tree a module family grows into, rebuilt from titles: a page nests under the page whose title is its dotted prefix, so "Net.DNS" (net/dns.kex) is a child of "Net" (net.kex), and "Net.HTTP.WebSocket" of "Net.HTTP". The longest existing prefix wins; a prefix with no page of its own ("Control" for "Control.Retry") leaves the page at the top level. Titles, not paths, carry the relation — the module names are what a reader scans for. ### `parentLoop` ```kex parentLoop(parts: [String], pages: [SourcePage], page: SourcePage) -> String? ``` ### `claimedByAnother?` ```kex claimedByAnother?(pages: [SourcePage], page: SourcePage, title: String) -> Bool ``` ### `topLevelPages` ```kex topLevelPages(pages: [SourcePage]) -> [SourcePage] ``` The sidebar's top level: pages nothing parents, alphabetically by title — a reader scans by name, not by directory-walk order. ### `childPages` ```kex childPages(pages: [SourcePage], title: String) -> [SourcePage] ``` The pages nested directly under `title`, alphabetically. ### `descendantPages` ```kex descendantPages(pages: [SourcePage], page: SourcePage) -> [SourcePage] ``` Every page in `page`'s family at any depth, alphabetically — what a module page lists as its submodules. ### `sortedByTitle` ```kex sortedByTitle(pages: [SourcePage]) -> [SourcePage] ``` ### `buildLinks` ```kex buildLinks(pages: [SourcePage]) -> [LinkEntry] ``` Linkable names, unique-target-wins: a name links while exactly one (page, anchor) answers it. A qualified name ("FS.Path") is its own name; a simple one ("Path") survives only while nothing else claims it — once two different entities answer "Request", the simple spelling stops linking and only the spelled-out names do. ### `withTypeHomes` ```kex withTypeHomes(links: [LinkEntry], types: [Tey.Docgen.Model.TypeFacts]) -> [LinkEntry] ``` The links, plus a home for every type that make blocks extend. A bare `Integer` is claimed by a module (number parsing) AND by five make blocks, so the uniqueness rule drops it — yet a type name in a signature means the type, and the type has a home page (Relations.homePathOf). Those homes fill in what uniqueness left out; a name that already links keeps its link. ### `linkEntriesOnPage` ```kex linkEntriesOnPage(entities: [Entity], urlPath: String) -> [LinkEntry] ``` ### `memberLinks` ```kex memberLinks(ownerName: String, owner: String, functions: [Tey.Docgen.Model.FunctionEntry], urlPath: String) -> [LinkEntry] ``` `List.map`, `Enumerable.filter`: members are linkable under their owner's name, so prose that writes `List.map` lands on the method. ### `linkableNames` ```kex linkableNames(e: Entity) -> [String] ``` A type/reference name links under its qualified name; entities whose qualified name is the bare name (top-level declarations) offer that spelling too. A make's target with type parameters ("[X]", "Map") links under the base name before the "<" — the anchor is still the full-target one, matching the section id. ### `uniqueTarget?` ```kex uniqueTarget?(entries: [LinkEntry], candidate: LinkEntry) -> Bool ``` Whether `candidate`'s name answers to exactly one target: every other entry under the same name must land on the same (page, anchor). ### `differentTarget?` ```kex differentTarget?(a: LinkEntry, b: LinkEntry) -> Bool ``` ### `resolve` ```kex resolve(links: [LinkEntry], name: String) -> LinkEntry? ``` ### `segmentsOf` ```kex segmentsOf(typeText: String) -> [String] ``` Splits a type reference into identifier segments and separator runs, so a renderer can link the identifiers and leave the punctuation alone: "Map" → ["Map", "<", "String", ", ", "FS.Path", ">"]. ### `splitLoop` ```kex splitLoop(rest: String, ident: String, sep: String, out: [String]) -> [String] ``` `ident` is the identifier being read, `sep` the separator run since it; only one is non-empty at a time, and whichever it is goes out in order. ### `flushIdent` ```kex flushIdent(ident: String, out: [String]) -> [String] ``` ### `flushSep` ```kex flushSep(sep: String, out: [String]) -> [String] ``` ### `isIdentChar?` ```kex isIdentChar?(ch: String) -> Bool ``` ### `hrefFrom` ```kex hrefFrom(fromPath: String, toPath: String, anchor: String) -> String ``` The href from one page of this package to another, with anchor: same page → "#anchor"; else up to the version directory and back down. ### `upsToVersionDir` ```kex upsToVersionDir(urlPath: String) -> String ``` ### `ups` ```kex ups(n: Integer) -> String ``` --- --- package: tey version: "0.2.1-dev" source: tey/docgen/prose.kex title: Tey.Docgen.Prose entities: - { kind: module, name: "Tey.Docgen.Prose" } --- # Tey.Docgen.Prose `tey docs prose`: a tree of Markdown files rendered into the same chrome as the generated reference. The unit is the same one `build` documents — a named, versioned thing — so the two land side by side in one site and share `versions.json`, the stylesheet and the layout. What differs is only where the pages come from: `build` parses .kex and derives them, `prose` reads .md and renders it. Nothing here knows what the prose IS. A guide, a book, a package's own handbook are all "long-form pages for a versioned unit"; the naming and the assembly of a site out of several such units belong to whoever runs this, not to docgen. ## module `Tey.Docgen.Prose` ### `proseCommand` ```kex proseCommand(parsed: OptionParser.ParsedOptions) -> Integer ``` ### `readPages` ```kex readPages(files: [String], source: String, accum: [ProsePage]) -> [ProsePage] ``` ### `pageOf` ```kex pageOf(fileName: String, text: String) -> ProsePage ``` ### `stripSuffix` ```kex stripSuffix(name: String, suffix: String) -> String ``` ### `writeProse` ```kex writeProse(pages: [ProsePage], versionDir: String, out: String, model: PackageModel) -> () ``` Both renderings, from the same parsed page: HTML for readers, Markdown for crawlers, LLMs, and the "Markdown" link every page carries — the same `.md`-next-to-`.html` pairing reference builds write. ### `writeProseLoop` ```kex writeProseLoop(pages: [ProsePage], index: Integer, versionDir: String, model: PackageModel) -> () ``` ### `proseIndexHtml` ```kex proseIndexHtml(pages: [ProsePage], model: PackageModel) -> String ``` The book's own spine: chapters in author order with their descriptions, not the module grid reference builds get. Deliberately separate from `versionIndexHtml` — `tey docs build` keeps documenting any package, and nothing about a book leaks into it. ### `chapterItem` ```kex chapterItem(prose: ProsePage) -> String ``` ### `prevNextHtml` ```kex prevNextHtml(pages: [ProsePage], index: Integer) -> String ``` Chapter footer: the siblings around this one, so the book reads forward as well as sideways. First and last chapters link one way only. ### `prevLink` ```kex prevLink(pages: [ProsePage], index: Integer, up: String) -> String ``` ### `nextLink` ```kex nextLink(pages: [ProsePage], index: Integer, up: String) -> String ``` ### `chapterNav` ```kex chapterNav(prev: String, nextHtml: String) -> String ``` ## record `ProsePage` A page and the things only its frontmatter knows: where it sorts, the HTML its Markdown became, the description its index entry shows, and the Markdown (minus frontmatter) its raw `.md` rendering serves. **Fields** - `page` : [SourcePage](../../tey/docgen/model.md#record-tey-docgen-model-sourcepage) - `order` : [Integer](../../../../prelude/0.4.0-rc.3-dev/number.md#make-integer) - `body` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `description` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) - `markdown` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string) --- --- package: tey version: "0.2.1-dev" source: tey/docgen/rdoc.kex title: Tey.Docgen.Rdoc entities: - { kind: module, name: "Tey.Docgen.Rdoc" } --- # Tey.Docgen.Rdoc RDoc-style doc-comment text → structured documentation (typed). A doc comment is free prose followed by directive paragraphs: ```kex # Sums all elements. Returns +0+ for an empty list. # # @param f [X -> Number] maps each element # @return [Number] # @example # [1, 2].sum # => 3 ``` Inline ``code`` pairs are rewritten to `` `code` ``. The result is a Tey.Docgen.Model.Doc record. ## module `Tey.Docgen.Rdoc` ### `parseDoc` ```kex parseDoc(text: String) -> Doc? ``` ### `parseOrEmpty` ```kex parseOrEmpty(text: String) -> Doc ``` ### `rewriteProseBlocks` ```kex rewriteProseBlocks(blocks: [[String]]) -> [[String]] ``` ``code`` is prose markup, so it is rewritten per prose block — after the split, not before it. Rewriting the whole comment at once paired a `+` operator inside an indented code sample (say `1 + 1`) with the next prose pair (``describe``), turning the operator into a stray backtick and eating the markup. Verbatim blocks and `@example` code are Kex source and are left alone; `@param`/`@return` descriptions are prose and still rewrite. Pairing never crosses a block: each block is rewritten on its own. ### `rewriteProseBlock` ```kex rewriteProseBlock(block: [String]) -> [String] ``` ### `rewritePlusPairs` ```kex rewritePlusPairs(s: String, accum: String) -> String ``` ### `plusPair?` ```kex plusPair?(head: String, inner: String, tail: String) -> Bool ``` A ``...`` pair is markup only when it reads as one: content with no space against either delimiter (so `1 + 1` stays an addition), no backtick inside (so converted spans never nest), and a non-identifier character on both sides (so `a+b` stays put even if it ever reaches here). ### `boundaryBefore?` ```kex boundaryBefore?(head: String) -> Bool ``` ### `boundaryAfter?` ```kex boundaryAfter?(tail: String) -> Bool ``` ### `splitParagraphs` ```kex splitParagraphs(lines: [String], current: [String], accum: [[String]]) -> [[String]] ``` Splits lines into paragraphs (blocks). Prose groups until a blank line; directives are LINE-based, so each `@param`/`@return`/`@example` line begins its own paragraph — consecutive `@param a` / `@param b` lines must not merge into one block, or only the first would be parsed. An `@example`'s indented code lines follow it and stay in its block (they do not open with `@`). ### `directiveBlock?` ```kex directiveBlock?(current: [String]) -> Bool ``` ### `indented?` ```kex indented?(line: String) -> Bool ``` ### `indentWidth` ```kex indentWidth(s: String) -> Integer ``` RDoc's verbatim marker is indentation: a prose block whose every line is indented is code — or a hand-laid-out table — that the author positioned deliberately. Reflowing it into a paragraph loses both the line breaks and the highlighting, so it is carried through the summary as a fenced block instead, which both renderers turn back into a highlighted `
`.

Leading-space count of a line.

### `verbatim?`

```kex
verbatim?(lines: [String]) -> Bool
```

### `dedent`

```kex
dedent(lines: [String]) -> [String]
```

Strips the common indentation shared by every non-blank line, so a block written at two spaces (or nested deeper) renders flush left.

### `fenceVerbatim`

```kex
fenceVerbatim(lines: [String]) -> String
```

### `mergeVerbatim`

```kex
mergeVerbatim(blocks: [[String]], accum: [[String]]) -> [[String]]
```

A blank line inside a verbatim block ends a paragraph but not the block, so adjacent verbatim blocks are re-joined with the blank line between them.

### `classifyBlocks`

```kex
classifyBlocks(blocks: [[String]], summary: String, params: [Param], returns: Return?, examples: [Example], deprecated: String?) -> Doc?
```

### `directiveBody`

```kex
directiveBody(lines: [String], prefixLength: Integer) -> String
```

### `parseParam`

```kex
parseParam(lines: [String]) -> Param
```

**Parameters**

  - `name` — description

### `parseReturn`

```kex
parseReturn(lines: [String]) -> Return
```

Reads a `@return [Type] description` directive.

### `parseExample`

```kex
parseExample(lines: [String]) -> Example
```

### `parseBracketed`

```kex
parseBracketed(s: String) -> Bracketed?
```

"[...]" prefix → Bracketed. None when the text does not open with a bracketed group.

### `findClosingBracket`

```kex
findClosingBracket(s: String, index: Integer, depth: Integer) -> Integer?
```

## record `Bracketed`

**Fields**

  - `content` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)
  - `rest` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)




---

---
package: tey
version: "0.2.1-dev"
source: tey/docgen/relations.kex
title: Tey.Docgen.Relations
entities:
  - { kind: module, name: "Tey.Docgen.Relations" }
---

# Tey.Docgen.Relations

Cross-file relations: which make blocks extend which type, which traits a type implements, and who implements each trait.

Extraction documents one file at a time, but a reader thinks in types. The methods of `Integer` live in number.kex, time.kex, units.kex and algebra.kex; the fact that a `List` is `Enumerable` — and so has `map`, `filter` and `find` — is written on a make block, not on the type. These indexes are what let the List page say where its methods come from, and the Enumerable page say who implements it.

## module `Tey.Docgen.Relations`

### `build`

```kex
build(pages: [SourcePage]) -> ([TypeFacts], [TraitFacts])
```

### `pageExtensions`

```kex
pageExtensions(page: SourcePage, entities: [Entity]) -> [Extension]
```

### `extensionOf`

```kex
extensionOf(page: SourcePage, entity: Entity, entry: MakeEntry) -> Extension
```

### `typeFacts`

```kex
typeFacts(base: String, extensions: [Extension], pages: [SourcePage]) -> TypeFacts
```

### `homePathOf`

```kex
homePathOf(base: String, mine: [Extension], pages: [SourcePage]) -> String
```

Where a reader should land for a type: the page declaring it; else the page titled after it (string.kex for the builtin String); else the page whose make block adds the most methods — number.kex for Integer, not algebra.kex, which only makes it a Monoid.

### `homeAnchorOf`

```kex
homeAnchorOf(base: String, home: String, mine: [Extension], pages: [SourcePage]) -> String
```

### `declarationAnchors`

```kex
declarationAnchors(entities: [Entity], base: String) -> [String]
```

### `companionModule?`

```kex
companionModule?(entities: [Entity], base: String) -> Bool
```

### `declares?`

```kex
declares?(entities: [Entity], base: String) -> Bool
```

### `pageTraits`

```kex
pageTraits(page: SourcePage, entities: [Entity], extensions: [Extension]) -> [TraitFacts]
```

### `namesTrait?`

```kex
namesTrait?(written: String, name: String, qualifiedName: String) -> Bool
```

`implement: Enumerable` and `implement: Kex.Enumerable` both name the trait declared as `Enumerable` inside `module Kex`.

### `typeFactsFor`

```kex
typeFactsFor(model: PackageModel, base: String) -> TypeFacts?
```

The facts for the type a make target extends, if any make extends it.

### `traitFactsFor`

```kex
traitFactsFor(model: PackageModel, written: String) -> TraitFacts?
```

### `traitEntryFor`

```kex
traitEntryFor(model: PackageModel, facts: TraitFacts) -> TraitEntry?
```

The declaration of a trait, for its method list.

### `traitEntries`

```kex
traitEntries(entities: [Entity]) -> [TraitEntry]
```

### `traitMethods`

```kex
traitMethods(model: PackageModel, entry: TraitEntry) -> ([FunctionEntry], [FunctionEntry])
```

A trait's methods as an implementor sees them. Required: declared with no body and given no default anywhere. Provided: a body in the trait, or a default from a `make ` block in any file — plus methods such a block adds that the trait never declared.

### `traitDefaults`

```kex
traitDefaults(entities: [Entity], entry: TraitEntry) -> [FunctionEntry]
```

### `traitTarget?`

```kex
traitTarget?(model: PackageModel, target: String) -> Bool
```

Whether a make target names a trait of this package.

### `allFunctions`

```kex
allFunctions(entities: [Entity]) -> [FunctionEntry]
```

Every function documented anywhere in these entities: top-level and module functions, trait methods, make members.

### `required?`

```kex
required?(f: FunctionEntry) -> Bool
```

A trait method with no clause is one an implementor must write; one with a body comes for free.


---

---
package: tey
version: "0.2.1-dev"
source: tey/docgen/scan.kex
title: Tey.Docgen.Scan
entities:
  - { kind: module, name: "Tey.Docgen.Scan" }
---

# Tey.Docgen.Scan

Source discovery: walk a tree and collect the files a build reads.

`spec/` directories are skipped: specs document the same declarations as their base file, so a second page for them would duplicate every entry.

## module `Tey.Docgen.Scan`

### `sourceFiles`

```kex
sourceFiles(sourceDir: String) -> [String]
```

### `proseFiles`

```kex
proseFiles(sourceDir: String) -> [String]
```

The prose half of the same walk: `tey docs prose` reads .md rather than .kex, and wants the same recursion, the same sorting and the same skip.

### `filesWithSuffix`

```kex
filesWithSuffix(sourceDir: String, suffix: String) -> [String]
```

### `subdirectories`

```kex
subdirectories(sourceDir: String) -> [String]
```

### `nested`

```kex
nested(dirs: [String], sourceDir: String, suffix: String) -> [String]
```


---

---
package: tey
version: "0.2.1-dev"
source: tey/docgen/serve.kex
title: Tey.Docgen.Serve
entities:
  - { kind: module, name: "Tey.Docgen.Serve" }
---

# Tey.Docgen.Serve

Serves a generated output directory over HTTP. Tey runs on the BEAM, so Net.HTTP.Server is available wherever `tey docs -- serve` runs.

## module `Tey.Docgen.Serve`

### `serveCommand`

```kex
serveCommand(parsed: OptionParser.ParsedOptions) -> Integer
```

### `routesFor`

```kex
routesFor(prefix: String, name: String) -> [String]
```

The URL paths a browser would type for this file: an index.html is reached by its directory ("/", "/prelude/0.4.0-alpha/") as much as by its name, and the router matches exactly — so it answers the bare, the slash-terminated, and the named spelling. Every other file answers its own path only.

### `collectServeFiles`

```kex
collectServeFiles(dir: String, prefix: String) -> [ServedFile]
```

### `serveFile`

```kex
serveFile(filePath: String, _request: Request, _context: Context) -> Response
```

A route handler once `filePath` is applied: `~serveFile(path)`. Reads the file as bytes, not text: the site ships images and fonts beside its HTML, and decoding those as UTF-8 would turn them into a 404.

### `contentTypeFor`

```kex
contentTypeFor(path: String) -> String
```

## record `ServedFile`

**Fields**

  - `route` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)
  - `path` : [String](../../../../prelude/0.4.0-rc.3-dev/string.md#make-string)