d6 docs

Collections and Enumerable

Language reference · Previous: Gradual typing and exceptions · Next: Runtime features and debugging

[1, 2] for arrays and {"key": value} for hashes, both with optional type parameters, and both can be split across lines — a newline is allowed right after the opening bracket, right after each ,, and right before the closing bracket, the same as every other bracket-delimited, comma-separated list in the language (call arguments, def parameter declarations, generic type-argument lists, and more):

config = {
  "name": "myapp",
  "values": [
    1,
    2,
  ],
}

An array literal can splat one Array into place: [head, *rest], [*items, last], or [first, *middle, last] build a new Array (a non-Array operand raises TypeError). Only one splat per literal is supported; join more with concat.

A hash literal also accepts Ruby's rocket separator, {"a" => 1, 3 => "three"}, which means exactly the same as {"a": 1}; the two can be mixed in one literal. The key is an expression either way, so {a => 1} uses the value of a, and a bare name: value is not a symbol-key shorthand.

Array.new(n) builds an Array of n nils and Array.new(n, fill) one of n copies of fill (Array.new(3, 0) is [0, 0, 0]). As in Ruby, every slot holds the same fill object, not a copy of it: Array.new(2, []) is two references to one inner Array. n must be an Int (a TypeError, or a compile error for a literal of another type) and not negative (ArgumentError). Array.new(n) do |i| ... end instead fills each slot with the block's value for that index (Array.new(3) do |i| i * i end is [0, 1, 4]).

Hash.new() is an empty Hash, and Hash.new(default) one whose missing keys read as default instead of nil, so counts = Hash.new(0) then counts[word] += 1 counts words. hash.default() returns it. Reading a missing key does not store it, every missing key answers the same default object (so Hash.new([]) shares one Array, as in Ruby), and the default is fixed when the Hash is made. dup and copies into another thread keep it; methods that build a new Hash (merge, select, transform_values, ...) do not.

.push, .pop, and .length() are native on both. Array additionally has .join(separator = ""), native and O(n) total (a StringBuilder- backed accumulator internally, not repeated string concatenation) — stringifies each element (the same formatting string interpolation uses, including calling a user-defined to_s override) and joins them with separator between (not trailing). .slice(start, length) works the same way String#slice does (below): start in 0..count, length >= 0, clamped to what's actually available rather than erroring past the end. array[a..b] and array[a...b] take a Range the same way, and a negative bound counts from the end (array[1..-1] is everything but the first element; array[0...-1] everything but the last); a start before the beginning or past the end raises IndexError.

== on both is real structural (not identity) comparison: Array by length plus elementwise equality, order-sensitive; Hash by entry count plus every key present in both with an equal value, order-insensitive ({"a": 1, "b": 2} == {"b": 2, "a": 1} is true). Recursive to a depth of 256 (DIAMOND_STRUCTURAL_MAX_DEPTH), deep enough for any real nesting while still bounding a self-referential Array/Hash (a = []; a.push(a)) — two different cyclic values compare unequal rather than hanging, and a cyclic value compared to itself short-circuits to true via an identity check before any recursion happens. Hashing matches: an Array/Hash used as a Hash key is hashed by the same structural content (Array order-sensitive, Hash order-insensitive, mirroring their own ==), so h[[1, 2, 3]] finds a value inserted through a different [1, 2, 3] object with equal contents. Instance is unrelated to any of this — it's still identity-only unless its class defines its own == (see Classes and modules).

Array's Enumerable-style methods: .each/.select/.map/.reduce/ .count/.any?/.all? (shared with Hash, driven by .each), .sort/.sort_by/.min/.max/.min_by/.max_by/.reject/.find/ .each_with_index/.sum, and .take(n)/.drop(n)/.flat_map/ .partition/.group_by/.zip(other)/.each_slice(n)/.each_cons(n)/ .tally/.take_while/.drop_while. any?, all?, none? and one? without a block test the elements themselves (only nil and false fail); min(n)/max(n) return the n smallest or largest as an Array; sum(start) adds onto start instead of 0 (sum(0.0), sum("")), with or without a block. sort and sort_by are stable merge sorts (O(n log n), and sort_by calls its block once per element); a blockless sort of an array that is all Ints or all Strings runs natively. sort() do |a, b| ... end orders by a block that returns a negative, zero or positive Int. Arrays compare element by element and then by length when sorting or taking a min/max, so words.sort_by() do |w| [w.length(), w] end orders by length, then alphabetically. Calling .lazy() on an Array, Range, or other Enumerable returns a LazyEnumerator. Its map, select, and reject operations are composable and deferred until a terminal runs; chained transforms do not allocate intermediate Arrays. each, to_a, and force drain the pipeline. take, find, any?, and all? short-circuit through the cooperative each_until protocol; Arrays, Hash values, and Ranges stop pulling as soon as the terminal has its answer. The remaining methods retain their eager behavior. The last group returns its result directly — .each_slice/.each_cons collect every slice/window into an Array[Array] up front (with a block, each one is also passed to it), .partition returns [matching, non_matching], .group_by a Hash keyed on the block's own result, .tally a Hash of element to occurrence count, and .zip pads the shorter array with Nil out to the receiver's own length ([1, 2, 3].zip([4, 5]) => [[1, 4], [2, 5], [3, nil]]), matching Ruby. The Hash versions are listed under Prelude methods below. Strings support .length(), .index_of(needle) (position or nil), .slice(start, length), .to_i()/.to_f() (lenient decimal parsing — .to_f() additionally accepts exponent notation like "1e3" even though d6's own float literals don't, and overflows to Infinity rather than raising, unlike .to_i(); neither skips leading whitespace, and "nan"/"inf" parse as 0.0, matching Ruby's String#to_f), and .downcase()/.upcase() (ASCII-only case conversion), and .reverse() (byte-level, not UTF-8-grapheme-aware — consistent with the rest of the language having no other Unicode awareness), and .strip() (trims leading/trailing ASCII whitespace; .lstrip()/.rstrip() trim only one side), .chomp() (removes one trailing "\n", "\r\n" or "\r"; .chomp(suffix) removes suffix instead, when the string ends with it), .capitalize() (first ASCII letter upper, the rest lower), .chars() (an Array of one-byte strings, so a multi-byte UTF-8 character becomes several), and .empty?() (zero length, like Array's and Hash's own empty?). .split(separator) returns an Array of every piece around non-overlapping occurrences of separator (an empty separator splits into one-character strings); unlike Ruby, it keeps every piece including empty ones from consecutive or leading/trailing separators (no trailing-empty suppression) — a deliberate simplification, not an attempt at Ruby compatibility. separator can also be a Regexp (see Runtime features): splits on every match instead of a literal substring, collecting the text between matches (and whatever's left after the last match, or the whole string when there's no match at all). A zero-width match is skipped rather than split on, matching .scan's own zero-width handling. Any other separator type raises TypeError. .ord() returns the first byte's value as an Int; an empty String raises a rescuable IndexError. Int#chr() is its inverse, also available as the global chr(code) (recognized the same way gets() is, so shadowable by a local or top-level function), returning a one-character String. A code outside 0..255 raises a rescuable RangeError.

.format(values) is a sprintf-style formatter — values is either a single value or an Array of them. format % values is the same thing, spelled as in Ruby:

"Name: %s, Age: %d".format(["Alice", 30])  # => "Name: Alice, Age: 30"
"%05d".format(42)                          # => "00042"
"%-10s|".format("hi")                      # => "hi        |"
"%.2f" % 3.14159                           # => "3.14"
"%s=%d" % ["hits", 3]                      # => "hits=3"

Directives: %d/%i (Int or Float, truncated), %f (Float or Int, default 6 decimal places, %.Nf for N), %x/%X/%o/%b (Int, hex/octal/binary — %b has no C printf equivalent, hand- rolled), %s (any value, via the same formatting string interpolation uses, including a user-defined to_s override), and %% for a literal %. - left-justifies and 0 zero-pads within a numeric width prefix (%-10s, %05d); .N sets %f's precision. Every directive's argument type is checked against what that directive actually needs (TypeError on a mismatch); too few arguments raises ArgumentError, extra arguments are silently ignored. Each directive is handled by building a small, internally-chosen conversion string from the parsed flags/width/precision and handing it to a real snprintf alongside exactly one correctly-typed value — never your format string forwarded into a C varargs call directly, which would be a real format-string vulnerability given a d6 value's runtime type has no fixed relationship to what a positionally-matched C conversion expects.

Strings also support [] with a single Int index, returning a new one-character String (bounds-checked, IndexError outside the string — the same as .slice(); a negative index counts from the end, so s[-1] is the last byte), s[start, length], the same as s.slice(start, length), and s[a..b] / s[a...b] with a Range (a negative bound counts from the end, so s[1..-1] drops the first byte; a start outside the string raises IndexError). Unlike Array/Hash, []= on a String is rejected outright with a TypeError (strings are immutable). .repeat(n) (or s * n) returns a new String with the receiver repeated n times (n == 0 → ""); a negative n raises a rescuable RangeError.

.start_with?(prefix, ...)/.end_with?(suffix, ...) are plain literal String checks (no Regexp support); given several candidates, they are true when any one matches. .ljust(width, padding)/.rjust(width, padding) pad the receiver on the right/left with padding (repeated and truncated as needed) until it reaches width, or return the receiver unchanged if it's already at least width long; a negative width or an empty padding both raise a rescuable ArgumentError rather than looping forever. .tr(from, to) is Ruby-style character-set translation: from/to support a-z-style ranges and a leading ^ to negate from, with \ escaping a literal - or ^; each character in the receiver found in from is replaced by the character at the same position in to (a shorter to maps every remaining from character onto its own last character, matching Ruby), or deleted outright when to is "". An empty from raises ArgumentError; a non-String argument to any of these raises TypeError.

"hello".start_with?("he")          # => true
"main.d6".end_with?(".rb", ".d6")  # => true
"abc".ljust(6, "-")                # => "abc---"
"abc".rjust(6, "-")                # => "---abc"
"hello".tr("el", "ip")             # => "hippo"
"hello".tr("aeiou", "*")           # => "h*ll*"

Array#pack(template) and String#unpack(template) / unpack1 convert between values and binary Strings: integers C c S s L l Q q (little endian), n N (big endian), v V (little endian); strings a A Z; hex H; UTF-8 code points U; x (a null byte). A directive takes a count or * for all that remain: [1, 513].pack("CS"), data.unpack("N*"), "\xde\xad".unpack("H*"). unpack returns nil for an integer past the end of the data. Floats and the _ ! < > modifiers are not supported; an unknown directive raises ArgumentError.

Regexp literals: /pattern/ is the same as Regexp.new("pattern"), with the flags i (ignore case), m (. matches newline) and x (extended) after the closing slash: "a1 b22".scan(/[0-9]+/), /hello/i.match?(text), when /^cost/. A slash is a regex only where an expression can start; right after a value (a name, number, string, ), ], } or end) it is division, so x /2/ 3 stays arithmetic and puts /x/ needs parentheses: puts(/x/). Write \/ for a slash inside the pattern. Literals do not interpolate (#{} is a compile error; build the pattern with Regexp.new), and the literal compiles each time it is evaluated, so hoist one out of a hot loop.

.sub(pattern, replacement)/.gsub(pattern, replacement) replace the first/every match of pattern (a Regexp, or a String matched literally, as in Ruby: "a.b".gsub(".", "-") is "a-b") with replacement, which may contain Ruby-style backreferences (\0 the whole match, \1.. a capture group; a literal backslash is \\). Given a block instead of a replacement String, they pass each matched String to the block and substitute what it returns (converted with to_s): "a1 b22".gsub(Regexp.new("[0-9]+")) do |n| n.to_i() * 2 end gives "a2 b44". .scan(pattern) (a Regexp or a literal String) returns an Array of every match: the matched String itself when pattern has no capture groups, or an Array of that match's captures when it does -- never a mix of both across one call, since a given Regexp either has groups or doesn't:

"hello world".gsub(Regexp.new("o"), "0")                  # => "hell0 w0rld"
"2024-01-15".gsub(Regexp.new("(\\d+)-(\\d+)-(\\d+)"), "\\3/\\2/\\1")  # => "15/01/2024"
"a1 b22 c333".scan(Regexp.new("[0-9]+"))                   # => ["1", "22", "333"]
"key1=val1;key2=val2".scan(Regexp.new("([a-z0-9]+)=([a-z0-9]+)"))
# => [["key1", "val1"], ["key2", "val2"]]

See the Regexp guide for pattern/option syntax.

.each(callback), and the Enumerable methods derived from it — .select/.count/.any?/.all?/.reduce/.map — work as receiver syntax on both arrays and hashes (a hash's Enumerable operates over values, discarding keys), forwarding at runtime to ordinary d6 functions in the prelude:

[1, 2, 3, 4].select(is_even)
{"a": 1, "b": 2}.count(is_positive)
[1, 2, 3].reduce(0, add)

Native collections are extensible from d6 source without VM changes. Defining array_name(values, ...), hash_name(values, ...), or the shared fallback enumerable_name(values, ...) exposes .name(...) on the matching receiver. A trailing predicate ? is omitted from the bridge function name, so array_large(values, minimum) implements values.large?(minimum). Built-in VM operations take precedence over extension bridges.

Any user-defined class gets those same six methods, plus to_a, sort, sort_by, min, max, min_by, max_by, reject, find, each_with_index, sum, take, drop, flat_map, partition, group_by, zip, each_slice, each_cons, and tally, for free by implementing its own each(callback) and include-ing Enumerable. The first six forward straight to prelude functions driven by self.each(...), so they're generic by construction; the rest reuse Array's own existing, already-tested index-based implementations by materializing the receiver into an Array via to_a first (itself built on each) and delegating to those, rather than re-deriving each one generically over each().

For a user-defined class including Enumerable, blockless collection.sum() adds the values yielded by each, starting at integer zero. Empty collections return 0; mixed integer/float values produce a floating-point sum. Each call traverses the collection again. The included method takes no arguments or block; Array and Hash have their own block-taking sum implementations.

A handful of further Array/Hash conveniences work as receiver syntax too, forwarding the same way the Enumerable set above does: values.first()/values.last() return the first/last element (an empty array's IndexError propagates straight from the underlying []); values.first_or(fallback)/values.last_or(fallback) are the safe form, returning fallback instead when values is empty; values.empty?() returns whether the array has zero elements; values.include?(needle) returns whether any element == needle; values.index_of(needle) returns the position of the first such element, or nil (like String#index_of); values.reverse() returns a new array in reverse order; values.concat(other) (or values + other) returns a new array with other's elements appended; values.compact() returns a new array with any nil elements dropped; values.uniq() returns a new array with only the first occurrence of each distinct (==) element, order preserved (with a block, uniq() do |x| key end, elements are compared by the block's key); values.flatten() returns a new array with nested arrays fully flattened (recursively, matching Ruby's default), and values.flatten(depth) only that many levels; values.delete_at(index) mutates values in place (like the native .push/.pop), removing and returning the element at index, or nil without mutating if index is out of bounds.

hash.delete(key) removes key and returns its value, or nil when key is absent (a frozen Hash raises FrozenError). The remaining entries keep their insertion order, and a deleted key added again goes to the end. It is native and O(n) in the Hash's size, since later entries move down to keep the Hash dense; the result's static type is the Hash's value type or Nil. hash.clear() removes every pair and returns the same, now empty, Hash (a frozen Hash raises FrozenError, and extra arguments raise ArgumentError). It is native and does one O(n) pass over the Hash's table, where calling delete for each key would cost far more; the removed values themselves are not affected, only the Hash's references to them. hash.fetch(key, fallback) returns the value at key, or fallback (not raising) when key is absent; hash.keys()/hash.values() return an Array of the hash's keys/values respectively, both in insertion order; hash.include_key?(needle) returns whether needle is a key; hash.map_values(callback) returns a new Hash with the same keys and each value passed through callback; hash.empty?() returns whether the hash has zero pairs (same name and meaning as Array's own); hash.merge(other) returns a new Hash with the receiver's pairs then other's applied on top (other wins on key conflicts, unless a block is given: a.merge(b) do |key, old, new| old + new end stores what it returns). Like the rest of the Enumerable set, vm.c's native dispatch resolves each of these receiver calls to a same-named top-level prelude function (array_reverse, array_concat, ...) at runtime, so that free-function spelling still exists underneath and can't be removed without a native-dispatch rework -- receiver syntax is simply the only spelling documented and used going forward. array_join(values, separator = "") was the one exception: a thin wrapper around the already-native .join() above rather than .join()'s own implementation, with no dispatch dependency on its name, so it has been removed now that values.join(separator) is the only spelling.

Int/Float are scalar DiamondValues rather than heap objects, but native dispatch provides a small method surface. Fixed duration helpers .second(s)() through .week(s)() return numeric seconds, which compose with .ago()/.from_now() to produce wall-clock Time values; see the Time guide. General numeric helpers remain plain functions: abs(x)/min(a, b)/max(a, b)/mod(a, b), all accepting Int | Float (mixed Int/Float arguments auto-promote, same as arithmetic). abs inherits negation's overflow behavior, so abs of the most negative 64-bit Int promotes to an arbitrary-precision Int instead of raising or silently wrapping — there is no longer a most negative Int that abs can't represent. mod(a, b) is the C-style remainder, the counterpart of truncating division: unlike % (and /, which both floor), its result keeps the dividend's sign for both types — mod(-7, 3) is -1, not 2 (not Euclidean/ Python-style mod), and mod(-7.0, 3.0) is likewise -1.0. b == 0 raises the same ZeroDivisionError integer division would; b == 0.0 raises a rescuable RangeError instead (from to_i rejecting the resulting Infinity/NaN quotient — Float division by zero itself never raises, only the truncation step does).

sqrt(x), sin(x), cos(x), tan(x), exp(x), log(x), tanh(x), and pow(base, exponent) are native functions (no bytecode primitive to build on, same reasoning as chr/to_f/to_i) accepting Int | Float for every argument and always returning Float — pow(2, 10) is 1024.0, not 1024, even though both arguments are Int. No extra validation: results follow IEEE-754 directly, so sqrt(-1.0) is NaN rather than an error, the same philosophy Float arithmetic already uses throughout.

Tensor is a dense, row-major Float matrix — Tensor.zeros(rows, cols), Tensor.from_array(nested_array) (an Array of same-length Arrays of Int/Float), or Tensor.random(rows, cols, seed) (deterministic pseudorandom values in [-1, 1) from a plain LCG, filled directly in C — orders of magnitude faster than building the same values through Tensor.from_array and a d6-level loop, which matters once "rows x cols" reaches real model-weight sizes). Instance methods: #rows(), #cols(), #get(row, col), #set(row, col, value) (IndexError out of bounds, matching Array#[]), #matmul(other) (threaded, k-blocked; shape mismatch raises TypeError), #transpose() (a fresh copy), and #to_a() (back to an ordinary nested Array). Also a set of in-place elementwise/shape mutators and their backward passes, added once a real training loop (examples/transformer) showed per-element #get/#set round-trips, not #matmul, were the actual bottleneck at that example's own training-sized shapes: #add!(other)/#scale!(scalar)/ #add_bias!(bias)/#row_softmax!()/#gelu!() (forward mutators, each modifying the receiver in place and returning it), #column_sums()/ #columns(start, count)/#add_columns!(start, other)/#clone() (shape helpers), and #softmax_backward(grad_output)/#gelu_backward(grad_output) (called on the original input)/#layernorm_forward(gamma, beta, eps) (returns [output, normalized, inv_std], an Array[Tensor])/ #layernorm_backward(gamma, grad_output, inv_std) (called on layernorm_forward's own normalized result; returns [grad_x, grad_gamma, grad_beta]) — see examples/transformer/lib/tensor_ops.d6 for friendlier d6 wrappers around these, and that example's own gradcheck.d6 for every formula checked against numerical (finite-difference) gradients. Still deliberately narrow, not a general tensor library: no broadcasting, no non-2D shapes, and no autodiff built into Tensor itself — examples/transformer's own reverse-mode autograd (examples/transformer/lib/var.d6 and autograd.d6) is ordinary d6 code layered on top of these primitives, not a VM feature. It exists to measure real matmul/elementwise throughput (bench/-style native code, not boxed DiamondValue arrays) before committing to a fuller API surface — see examples/transformer/README.md's own "Scope" section for what a real training workload still needs that this doesn't provide (batching, a reshape/view story, anything beyond plain SGD).

Time.monotonic() returns a Float number of seconds from CLOCK_MONOTONIC — a duration-only clock: the value itself means nothing (not a calendar timestamp, not comparable across processes), only the difference between two readings does, e.g. elapsed = Time.monotonic() - start for timing a request in a rack middleware. It is intentionally separate from d6's wall-clock/calendar Time values; see the Time guide for those APIs.

BCrypt.hash(password: String, cost: Int) and BCrypt.verify(password: String, digest: String) -> Bool are native bcrypt password hashing, backed by this system's own libxcrypt (crypt_gensalt_rn/crypt_r, real $2b$ bcrypt) rather than a vendored implementation — the same "link a system library" pattern every other native dependency here already follows (sqlite3, libpq, mariadb, OpenSSL). .hash always takes both arguments explicitly (no default cost at this layer — see packages/active_record/README.md's #secure_password= for where a default of 12 actually lives); cost outside 4..31 raises ArgumentError before any hashing happens. .verify re-derives a digest from password using digest itself as the salt/settings source and compares with a constant-time comparison — a malformed or foreign digest (not a real bcrypt hash) is an ordinary false, not an exception, since checking a password against a bad hash is a normal outcome here, not a programmer error:

digest = BCrypt.hash("hunter2", 12)   # => "$2b$12$..."
BCrypt.verify("hunter2", digest)      # => true
BCrypt.verify("wrong", digest)        # => false

libxcrypt's bcrypt support is a Linux-specific fact about this system's crypt(3), not something d6 papers over — see docs/roadmap.md's "Explicitly deferred" section on multi-platform portability. A password containing an embedded NUL byte is truncated at that point before hashing, the same inherent limitation every C-crypt-backed bcrypt binding has (password is passed to crypt_r as a NUL-terminated C string).

SecureRandom.bytes(n: Int) -> String returns n cryptographically random bytes (OpenSSL RAND_bytes, already linked for TLS) as a raw d6 String — d6 strings are already raw byte buffers, so no separate binary type is needed. SecureRandom.hex(n: Int) -> String returns the same n random bytes hex-encoded, as a 2*n-character String. Both raise ArgumentError for a negative n (or one large enough to overflow the underlying int-sized call into OpenSSL). Suited to session/remember-me/password-reset tokens and similar — SecureRandom.hex(32) for a 256-bit token as a 64-character hex string.

SemVer compares and checks version strings, with the same grammar and precedence facet resolves with. A version may start with v, as a git tag often does.

  • SemVer.valid?(text: String) -> Bool
  • SemVer.compare(a: String, b: String) -> Int is -1, 0 or 1. Numeric prerelease identifiers compare as numbers, a prerelease sorts before its release, and build metadata is ignored.
  • SemVer.satisfies?(version: String, range: String) -> Bool takes the ranges Cutfile does (^1.2.0, ~1.2.0, >=1.0.0 <2.0.0). On 0.x a caret range doesn't cross a minor.

compare and satisfies? raise ArgumentError for a malformed version or range.

["1.10.0", "1.2.0", "1.2.0-rc.1"].sort() do |a, b| SemVer.compare(a, b) end
# ["1.2.0-rc.1", "1.2.0", "1.10.0"]
SemVer.satisfies?("0.4.1", "^0.4.0") # true

Digest.sha256(data: String) -> String returns the lowercase hexadecimal SHA-256 digest of the string's raw bytes. HMAC.sha256(key: String, data: String) -> String returns the corresponding keyed HMAC, also as 64 lowercase hexadecimal characters. Both preserve embedded NUL bytes and are backed by OpenSSL's libcrypto:

Digest.sha256("abc")
HMAC.sha256("secret", "payload")

HMAC.verify(data: String, key: String, signature: String) -> Bool recomputes HMAC.sha256(key, data) and compares it against signature with a constant-time comparison (CRYPTO_memcmp, the same primitive BCrypt.verify already uses) rather than a plain == on the two hex strings — verifying a signature is exactly the kind of comparison a timing side-channel matters for, unlike an ordinary string equality check. A signature of the wrong length, or not really hex at all, is an ordinary false, not an exception, matching BCrypt.verify's own "a bad input here is a normal outcome" reasoning:

signature = HMAC.sha256("secret", "payload")
HMAC.verify("payload", "secret", signature)   # => true
HMAC.verify("payload", "secret", "forged")    # => false

Digest.sha1(data: String) -> String and HMAC.sha1(key: String, data: String) -> String are the same pair, using SHA-1 instead (40 lowercase hexadecimal characters). SHA-1 is cryptographically weak for new signing use — this exists because TOTP (RFC 6238) mandates HMAC-SHA1 by spec, not as a second general-purpose recommendation alongside sha256. HMAC.verify has no sha1 counterpart; it only ever checks against HMAC.sha256.

Cipher.encrypt(key: String, plaintext: String) -> String and Cipher.decrypt(key: String, blob: String) -> String | Nil are native AES-256-GCM authenticated encryption, backed by OpenSSL's EVP_CIPHER API (already linked for TLS) rather than a vendored implementation. key must be exactly 32 raw bytes (SecureRandom.bytes(32)) — ArgumentError otherwise, since AES-256 has no meaningful way to silently accept a wrong-length key. .encrypt generates a fresh random 12-byte nonce every call and returns one binary-safe String: the nonce, the 16-byte GCM authentication tag, then the ciphertext, all concatenated — .decrypt needs nothing but the key and this one blob. .decrypt returns nil, not an exception, for a wrong key, a tampered or truncated blob, or anything else that fails GCM's own built-in tag check — a forged or expired encrypted cookie is expected to happen in normal operation, the same "not a programmer error" reasoning BCrypt.verify/HMAC.verify already use, and there is no separate signature step to add: the tag check inside EVP_DecryptFinal_ex already provides both confidentiality and tamper-evidence in one pass, with no comparison of d6's own to expose a timing side-channel.

key = SecureRandom.bytes(32)
blob = Cipher.encrypt(key, "sensitive session data")
Cipher.decrypt(key, blob)             # => "sensitive session data"
Cipher.decrypt(SecureRandom.bytes(32), blob)  # => nil (wrong key)

Gzip.compress(data: String) -> String and Gzip.decompress(data: String, max_size: Int) -> String are gzip-wrapped deflate, backed by the already-linked zlib. .decompress also accepts a plain zlib-wrapped stream (auto-detected) — some servers send Content-Encoding: deflate as one of these rather than raw deflate, and this covers both real-world spellings without the caller needing to know which. max_size bounds the decompressed output, checked as it grows rather than after the fact — decompressing a small, untrusted input into an unbounded output ("zip bomb") is a real risk for anything that automatically decompresses a network response, so this raises a rescuable IOError instead of ever fully materializing an over-cap buffer. Malformed input (corrupt or truncated) also raises IOError, not nil — unlike Cipher.decrypt, there's no expected-in-normal-operation forged-input case here to justify that softer failure mode.

compressed = Gzip.compress("some text")
Gzip.decompress(compressed, 10 * 1024 * 1024)  # => "some text"

Base64.encode(data: String) -> String and Base64.decode(data: String) -> String are the standard (RFC 4648) alphabet with padding, via OpenSSL's EVP_EncodeBlock/EVP_DecodeBlock (already linked). Needed for HTTP Basic auth (Authorization: Basic <base64>). .decode validates the character set itself before decoding (rather than trusting EVP_DecodeBlock's own lenience, which varies by OpenSSL version), so a malformed input (wrong length, an invalid character, misplaced = padding) always raises a rescuable ArgumentError with a clear message:

Base64.encode("hello world")              # => "aGVsbG8gd29ybGQ="
Base64.decode("aGVsbG8gd29ybGQ=")          # => "hello world"

See packages/cookies for signed and encrypted cookie helpers built on HMAC.verify/Cipher above.

array_sort(values: Array[Int]) returns a new sorted array (input untouched); Int is the only type with a native ordering comparison, so this is Int-only, checked up front (expected Array[Int], got Array on a non-Int element) rather than failing confusingly mid-sort.

StringBuilder (defined in the prelude, lib/core.d6) is the named escape hatch from result = result + piece in a loop — quadratic, since it reallocates and copies the whole accumulated string on every iteration:

sb = StringBuilder.new()
sb.append("hello")
sb.append(", world")
sb.to_s()        # => "hello, world"
"#{sb}"           # same, via to_s
sb.length()       # => 12

#append returns self, so calls chain: sb.append("a").append("b"). It's a plain d6 class, not a native object — #append pushes "#{piece}" onto an internal Array (already O(1) amortized) and #to_s calls the native .join("") above once, so the total cost of building a string this way is O(n), the same complexity Array#join already has for a pre-collected array of pieces.

Prelude methods

Beyond the methods above, built-in values answer most of the everyday Enumerable-style methods. They're ordinary d6 functions in the prelude (lib/core.d6, named diamond_array_…, diamond_hash_…, and so on) that the VM finds when a value has no native method of that name. A program can define its own array_foo(values, ...) (or hash_, string_, integer_, float_, numeric_) function to add a method, or to replace one of these.

  • Array: size, insert(i, x), unshift(x), shift, clear, replace(other), delete(x), delete_if, keep_if, fill(x), first(n), last(n), count() / count(x) / count { }, find_index/index/rindex (a value or a block), values_at, dig, to_h, transpose, minmax, sum { }, filter_map, each_with_object, none?, one? (both also blockless), inject, product, combination(k), chunk_while, slice_when, intersect?(other), zip with up to six arrays, rotate, shuffle, sample, to_a. Operators: a + b, a - b (elements not in b), a & b, a | b, a * n, a * ", " (join), and a negative index (a[-1]) or a[start, length].
  • Hash: a block that takes two parameters gets (key, value), as in Ruby; a one-parameter block still gets the value, as Hash iteration always has. map, select/filter and reject (a Hash for a two-parameter block), any?, all?, none?, count, find, sum, min_by, max_by, sort_by, sort, group_by, partition, filter_map, each_with_object (a three-parameter block gets (key, value, memo)), each_with_index, first / first(n) ([key, value] pairs), values_at / fetch_values (raising IndexError for a missing key), to_a, size, key?/has_key?/ include?/member?, value?, dig, transform_values, transform_keys, invert, update, slice, except, compact, delete_if/keep_if (in place, like Array's), and fetch(key) without a fallback (an IndexError for a missing key). any?, all? and none? without a block ask about the entries.
  • String: size, bytesize, index, rindex, count(chars), delete(chars), squeeze / squeeze(chars), index(needle, start), swapcase, lines, each_line, each_char, each_byte, center, chop, partition, delete_prefix, delete_suffix, casecmp?, between?, match?(regexp), to_sym, hex, oct, to_i(base), getbyte(i), split() (on whitespace), split(separator, limit), insert(index, other) and prepend(other) (new Strings, like every String method), and ljust/rjust with a default space pad.
  • Int and Float: ** (exact for an Int base and a non-negative Int exponent, a Float otherwise), pow, clamp, between?, divmod, fdiv, zero?, positive?, negative?, even?, odd?, succ, pred, gcd, lcm, digits, to_s(base), and on Float truncate, nan?, infinite?, finite?, and floor(digits)/ceil(digits)/ round(digits); on Int, step(limit, by) do |i| ... end (downward for a negative by).
  • Every value: to_s() (the text interpolation gives it) and nil?(); Callable#arity.
  • Math: Math.sqrt, cbrt, sin, cos, tan, exp, log, log2, log10, tanh, hypot, pi, e. The same builtins are also plain functions (sqrt(x)).
  • Range: step(n), first(n), reverse_each.

values_at, fetch_values, dig, slice, and except take up to six arguments, and zip up to six arrays.

Edit this page on GitHub