Repository navigation
Guide corelib
Mirrored from docs/guides/corelib.md in the repo — that is the canonical source; edits to this wiki page are overwritten when the wiki is re-synced.
Thesis context: The standard library tests the arena model against real-world patterns — I/O, crypto, serialization, hashing, HTTP. Every corelib function allocates into the caller's arena and returns independent values. Pure-Tycho modules (json, csv, sha256) prove the model handles production-like logic; C-shim modules (regex, http, crypto) prove the FFI boundary stays narrow without dragging C's memory model into Tycho's world.
corelib is Tycho's standard library: a set of packages under corelib/, imported with the
core: collection root. It's experimental, like the rest of Tycho, but the modules below
are usable today.
import "core:math"
fn main():
println(f"gcd={math.gcd(12, 18)}")
-
import "core:<pkg>"in your source. The name<pkg>is bound to the package. The transpiler findscorelib/next to its own binary by default, so no setup is needed in this repo. SetTYCHO_CORELIBto a corelib directory to override. - Call its functions either free-standing (
math.gcd(12, 18)) or method-style via UFCS (x.abs()).
import "core:<pkg>" resolves to <corelib>/<pkg> and binds the name <pkg>, where
<corelib> is TYCHO_CORELIB if set, otherwise corelib/ next to the transpiler binary.
Non-core: imports stay relative to the importing package, unchanged. Both Tycho transpilers
(tychoc and the self-hosted tychoc0) resolve core: natively, so tychoc0 is a
standalone corelib-aware transpiler with no bundling step required.
corelib is generic free functions over a type parameter $T, with where constraints
(comparable(T), numeric(T), has_str(T)) picking which element types each function
accepts. Tycho monomorphizes generics, so array and iterator utilities are single generic
packages over any element type [$T] (core:arrays, core:iter) rather than one package
per element type. The functions are also callable method-style through UFCS, so
index_of(xs, v) and xs.index_of(v) are the same call.
Higher-order helpers (map / filter / reduce that take a function) live in core:iter and
its siblings, and take a first-class fn/closure argument. Lambdas in Tycho are
expression-bodied; pass a named function where you need a multi-line body.
This generic, single-package shape keeps the surface small: one core:arrays covers every
element type instead of a family of per-type siblings.
-
math— scalar math.min/max/clamp(x, lo, hi)are generic over any comparable type (int/float/string/char) andsign(x)over any numeric type (returns an int −1/0/1);abs,gcd,ipow(base, exp)(exp ≥ 0) are integer- specific. (sqrt/pow/floor/fabsare float builtins.) -
fmath— float-only helpers (scalarmin/max/clamp/signlive inmath):pi,e,round(half away from zero),trunc,lerp(a, b, t),approx_eq(a, b, eps). (No trig — the language has no libm sin/cos builtin.) -
char— byte/char classification & conversion over int byte values (whats[i]/chruse):is_digit,is_alpha,is_alnum,is_upper,is_lower,is_space,is_hex,to_upper/to_lower(one byte),digit_val(−1 if not a digit),hex_val(0..15 or −1). The lexer/parser workhorse. -
strings—to_upper,to_lower,starts_with,ends_with,contains,repeat(s, n),trim(ASCII whitespace),parse_int,is_space,lines(splits on\n, drops one trailing\rper line, trailing newline adds no empty line),replace(non-overlapping, left to right; emptyoldreturns the input unchanged),count(s, sub),strip_prefix/strip_suffix,pad_left/pad_right(s, width, pad)(pad is one byte),reverse,capitalize,split_once(s, sep) -> (before, after)((s, "")if absent). (split/find/substr/len/chrare builtins.) -
path— POSIX path utilities, all pure string math (separator/, no filesystem access, every function returns a fresh value):base(final element, trailing slashes ignored;""→.,"/"→/),dir(all but the final element),ext(extension incl. the dot,""if none; a leading dot is a dotfile, not an ext),stem(base minus ext),join(a, b)(exactly one separator, empty operands drop out),is_abs,split_path(p) -> (dir, base)(the inverse ofjoin), andclean(lexical normalize: collapse//, drop., resolve..but never above the root or past a leading..).last_slash(s)is the shared scan helper. -
arrays— generic utilities over any element type[T]:contains,index_of(−1 if absent),count,reverse,take(xs, n),drop(xs, n),concat(a, b),fill(n, v),dedup(consecutive — sort first for a full dedup);sort(ascending),is_sorted,min,max(where comparable— int/char/float/string);sum,product(where numeric— int/float);join(xs, sep)(wherethe element converts to a string). All return a new array — value semantics, the input is never mutated.min/max/sum/productseed fromxs[0], so they need a non-empty array. (push/pop/len/rangeare builtins; higher-ordermap/filter/reducelive initer.) -
iter— generic higher-order helpers over any[T], each taking afn/closure:map,filter,reduce,count,any. (Predicates return an int used as a bool.) -
sort—argsort(keys)/argsort_desc(keys): return the index permutation that orders the keys — generic over any comparable key type (int/float/string/char). The way to order data by a derived value: keep it in parallel arrays, argsort one, and walk every array through the permutation. All stable. Plusby_key(xs, key): sort an array by a derived int key (a fn/closure). -
pool— a generational node pool for pointer-shaped data (graphs, trees, doubly-linked structures). Value semantics forbids a shared-mutable pointer graph, so the idiom is to hold every node in one array and link by integer index;poolpackages that idiom. Generic over the element typePool($T):add(&p, v) -> Handle,get(p, h),set(&p, h, v),remove(&p, h),alive(p, h),count(p). AHandleis a single packedintnewtype (pointer-sized, value-semantic, introduces no aliasing, and type-distinct from a rawintso you can't pass the wrong thing) carrying a slot index plus a generation; every access checks the generation, so a handle to a freed-and-reused slot is caught (use-after-free / double-free) at runtime instead of silently reading the new occupant. Create one withp := pool.Pool([]pool.Slot(int), []int). It improves ergonomics, not memory — pointer-shaped storage still costs ~1.55× C in this model (fundamental; see value-semantics limits) — what it removes is the hand-rolled index-plus-generation bookkeeping. -
rand— deterministic xorshift32 (not cryptographic). No globals in Tycho, so the state is an explicit int threaded viainout(the&marks theinoutcall site, Basics):st := rand.seed(42),rand.next(&st)([1, 2³²)),rand.below(&st, n)([0, n)),rand.shuffle(&st, xs)(Fisher-Yates, returns a new array). Every left shift is masked to 32 bits inside the signed 64-bit int, so the generator is UB-free by construction. -
time— wraps theclock()(monotonic ns) andnow()(UNIX seconds) builtins. Stopwatch (value-semantic, no inout — a reading is just an int):sw := time.start(), thentime.elapsed_ns(sw)/elapsed_us/elapsed_ms. Duration conversionsns_to_us/ns_to_ms/ns_to_s. Wall clockunix_secs()(named so, notnow, to avoid shadowing the builtin and recursing). -
datetime— civil (proleptic Gregorian) calendar math over UNIX timestamps, all pure integer arithmetic (Howard Hinnant'sdays_from_civil/civil_from_days, which port verbatim because tycho's int/truncates like C's). ADateTimestruct (year/month/day/hour/minute/second/weekday, all UTC).from_unix(secs)and its exact inverseto_unix(dt);days_from_civil/civil_from_days/weekday_from_days(the day-count core);weekday(y,m,d)(0=Sun..6=Sat),is_leap,days_in_month;now_utc()(the only non-pure fn — readsnow()); formattingformat_iso(YYYY-MM-DDTHH:MM:SS),weekday_name,month_name,pad2/pad4; and its inverse parsingparse_iso(wall-clock fields;Tor space separator, trailing zone ignored) /parse_iso_tz(+HH:MM/-HH:MM/Zfolded to the UTC instant), plusparse_clf/parse_clf_tzfor the Common Log Format stamp (dd/Mon/yyyy:HH:MM:SS ±HHMM, as in Apache/nginx access logs) andmonth_num("Jan".."Dec"→ 1..12), all fail-closed via ayear = -1sentinel thatok(dt)checks (a real 4-digit parse is never negative). The core is UTC; timezone support is layered on — fixed offsets (from_unix_at,to_unix_at,format_iso_tz) in pure Tycho, plus DST-aware system/zone offsets via a small libc shim (local_offset,offset_at,now_local). There is no IANA tz database. -
regex— POSIX extended regular expressions (ERE), the first C-shim-backed core module (FFI over<regex.h>, libc).compile(pat) -> ptr(opaque handle;ok/is_nullto check),is_match,find/find_end(offset or −1),matched(first match substring). Capture groups of the first match by index (0 = whole match):ngroups,group_start/group_end(offset or −1),group(re, s, n)(substring),groups(re, s)(all groups as[string]).releasefrees the C-owned handle — the compiled pattern is C-malloc'd, not arena-managed, so call it when done. -
http— an HTTP(S) client over libcurl (C-shim, FFI), and the first module to declare an external dependency (corelib/http/deps→libcurl).get(url) -> ptr/post(url, body, content_type) -> ptrreturn an opaque response handle, or null on a transport failure (ok/is_null).status(r)(e.g. 200),body(r)(arena-copied),release(r)(the handle is C-owned). Convenience:get_body(url)/get_status(url)do the request and free the handle. The body is arena-copied via the FFI string-return, so a binary body with interior0x00truncates — this is for text APIs. Tests are skipped where libcurl is absent. -
json— a recursive-descent JSON parser + serializer (theexamples/json.tydemo promoted to a reusable module). The document is a value-semantic tree, theJsonenum (JNull/JBool/JNum/JStr/JArr/JObj, objects as parallel key/value arrays).parse(s) -> Json(truncated input fails closed toJNull, no abort),stringify(j) -> string(compact, escapes" \ \n \t). Typed queries:kind(the variant tag as a string),get(j, key)(object field, elseJNull),at(j, i)(array element, elseJNull),keys(object keys in order),len_of(array/object count or string length),as_num/as_str/as_bool(payload with a zero-value default). Variants are constructible cross-package (json.JNum(1)) for building trees by hand. Scope: integers (no floats), the four common escapes. -
csv— an RFC 4180 CSV parser + serializer. A document is rows of fields,[[string]].parse(s) -> [[string]]is a small state machine handling quoted fields, the""escape, embedded delimiters/newlines inside quotes, and LF / CRLF / lone-CR line endings; it fails closed (an unterminated quote parses leniently, never aborts). A trailing newline adds no empty row; a mid-file blank line is[""].stringify(rows) -> stringemits LF endings and quotes only fields containing the delimiter/quote/CR/LF (doubling internal quotes) --parse/stringifyround-trip.parse_delim/stringify_delimtake an arbitrary single-byte delimiter (TSV isparse_delim(s, 9));get(rows, r, c)is a bounds-safe cell read (""if OOB). -
markdown— a pragmatic Markdown → HTML renderer in pure Tycho.render(src)handles ATX headings, fenced code (HTML-escaped), blockquotes, unordered/ordered lists, thematic breaks, and paragraphs; inline**bold**,*italic*/_italic_,`code`,[text](https://github.com/StefanVonRanda/tycho/tree/main/docs/guides/url),. All text is HTML-escaped and unknown syntax degrades to escaped plain text (never a parse abort). Not full CommonMark (no nested lists, reference links, tables, or setext headings) — targets a blog/wiki. -
base64— Base64 (RFC 4648)encode/decode, plusencode_url(URL-safe-/_alphabet, no padding). Pure arithmetic — the 6-bit packing uses/and%(exact on the unsigned 0..255 thats[i]returns), no bit-operators.decodeis lenient: it skips any non-alphabet byte (padding=, whitespace, newlines in wrapped Base64) and accepts both alphabets. Byte-safety caveat (a tycho string-model limit, not a Base64 one):encodeis fully byte-safe, butdecodebuilds its result withchr(), and a tycho string can't hold an interior0x00(chr(0)appends nothing), so decoding plaintext that contains a NUL byte silently drops it —decodeis exact for text and any non-NUL binary, lossy only for data containing0x00. -
hex— hexadecimalencode(lowercase) /encode_upper/decode, two digits per byte, plusis_valid(strict: even length, all hex digits).decodeis lenient (skips any non-hex byte, sode:ad:be:efand spaced/newlined hex work, either case) and reusescore:char'shex_val. Same0x00decode caveat asbase64(decode("00")is""). -
url— URL percent-encoding (RFC 3986).encodeis component-style (like JSencodeURIComponent— everything but unreservedA-Z a-z 0-9 - _ . ~becomes%XX, uppercase, space →%20);encode_form/decode_formuse thex-www-form-urlencodedconvention (space ↔+);decodedecodes%XX(leaving+).decodeis lenient (a%not followed by two hex digits is emitted literally) and reusescore:char'shex_val. Same0x00decode caveat asbase64/hex. -
uuid— random version-4 UUIDs (RFC 4122) plus the nil UUID and helpers.v4(&st)draws 16 bytes fromcore:rand(state threaded byinout) and sets the version/variant bits with plain arithmetic;nil(),parse(s) -> [int](16 bytes, lenient on separators,[]unless exactly 32 hex digits),format(bytes) -> string(canonical 8-4-4-4-12),is_valid(strict),version(the version nibble,-1if invalid). v1 and the name-based v3/v5 are out of scope (they need a MAC / MD5 / SHA-1, none of which corelib has). Bytes are formatted with an inline byte→hex, notchr(b), so zero bytes survive. -
hash— non-cryptographic 32-bit hashes for hash tables / checksums / dedup (NOT for security):fnv1a_32,djb2,sdbm, andcrc32(IEEE/zlib, bit-by-bit), plusto_hex(8-digit lowercase). All return a non-negative int in[0, 2^32)and are kept UB-free the same waycore:randis — values never leave[0, 2^32)and shifts/masks use* / %so no signed 64-bit overflow (only^among the bit-operators). Hashing only reads bytes, so there is no0x00caveat on the input. Matches published vectors:crc32("123456789") = cbf43926,fnv1a_32("foobar") = bf9cf968. -
md5— the MD5 message-digest (RFC 1321):hex(s)(32-char lowercase digest) anddigest(s)(16 raw bytes). Pure 32-bit arithmetic — adds masked with% 4294967296, the 32-bit NOT is4294967295 - x, and the left-rotate uses* / +(disjoint halves), all UB-free. MD5 is broken for security (use it for checksums / content-addressing / interop, never passwords or signatures). Bit-exact against the RFC 1321 suite (md5("abc") = 900150983cd24fb0d6963f7d28e17f72). -
sha256— the SHA-256 hash (FIPS 180-4):hex(s)(64-char lowercase digest) anddigest(s)(32 raw bytes). Pure 32-bit arithmetic, UB-free likemd5/hash; big-endian (words, length suffix, output). A real cryptographic digest — fine for checksums, content addressing, and HMAC building blocks (not a standalone password hash; use a KDF for that). Bit-exact against NIST vectors (sha256("abc") = ba7816bf…f20015ad). -
bignum— arbitrary-precision integers in pure Tycho (sign + base-10⁹ limb array), value-semantic:from_int/from_str/to_str/to_int,add/sub/mul/divmod/div/mod/pow,abs/neg/cmp/is_zero. -
decimal— arbitrary-precision base-10 fixed point, composed oncore:bignum(aBigcoefficient × 10⁻ˢᶜᵃˡᵉ), so decimal fractions are exact (0.1 + 0.2 == 0.3):from_str/to_str,add/sub/mul(exact),cmp,neg/abs,rescale(truncating). Division is deferred (needs a rounding policy). -
crypto— the security-grade module, a C-shim over OpenSSLlibcrypto(see C-shim modules; needs the OpenSSL dev package). Where the pure-Tychosha256/md5are for non-adversarial integrity, this is what you reach for when an attacker is in the threat model: CSPRNG (random_hex),sha256/sha512of binary,hmac_sha256,pbkdf2_sha256, constant-timect_equal, ChaCha20-Poly1305 AEAD (aead_encrypt/aead_decrypt,"!err"on auth failure), Ed25519 (ed25519_pubkey/sign/verify), and X25519 (x25519_pubkey/shared). Real crypto must be constant-time and audited, which pure Tycho can't promise, so the primitives are bound to OpenSSL rather than reimplemented. Every value (keys, nonces, ciphertext, signatures, digests) crosses as lowercase hex, because a Tycho string can't hold a0x00; usecore:hexto convert text. Checked against independent known-answer vectors (RFC 4231 HMAC, RFC 7914 PBKDF2, RFC 8439 ChaCha20-Poly1305, Ed25519/X25519). -
io— filesystem helpers over theread_file/write_file/list_dirbuiltins, and the first corelib module to compose others (importscore:stringsfor line splitting,core:pathforexists).read(p)(""if missing/unreadable),write(p, s)(truncate, returns false if unopenable),append(p, s)(read-rewrite, not atomic),read_lines(p)/write_lines(p, lines)(newline-terminated round-trip),list(p)(entry basenames),exists(p)(no exists builtin, so it lists the parent and checks membership). For inputs too large to slurp, a bounded-memory streaming line reader over a libcgetlineshim:open_lines(p)→read_line(r)(Some(line)/Noneat EOF) →close_lines(r), plusfold_lines(p, init, f)— peak memory is O(longest line), not O(file).read_bytes(p)reads a whole file as rawbytes(binary-safe — interior NUL bytes are preserved, unlikeread's string). Error model mirrors the builtins — nothing aborts. -
os— run external commands, via a libc-only FFI shim (popen/system; nodeps, nothing to install).os.system(cmd)runscmdthrough the shell with stdout/ stderr inherited, returning its exit code (0..255,128+signalif killed,-1if the shell won't start);os.run(cmd)additionally captures stdout intoOutput{code, out}.cmdis a/bin/sh -cline — shell metacharacters are active, so quote untrusted input yourself (no array-argv form yet). The stdout-capture read loop lives inos_shim.c, so Tycho only ever receives the finished, NUL-terminated string. -
net— TCP/UDP sockets over a libc-only FFI shim (net_shim.c, POSIX sockets; nodeps, nothing to install).listen/accept/connect/port_of/write/read/close_fdandudp_bind/udp_send/udp_read; fds areint(negative = failure), payloads are binary-safebytes. -
httpd— a minimal HTTP/1.1 server toolkit overcore:net(no external dependency — net is libc-only). The request/response plumbing is pure Tycho; you own the accept loop.parse_request(raw) -> Request(method/path/version, case-insensitiveheader(r, name), honors Content-Length;method == ""on a malformed line);response(status, body)/with_header(r, k, v)/render(r)(Content-Length and a defaulttext/plainContent-Type are added automatically); and the socket glueread_request(fd)(reads until the header terminator, then exactly Content-Length body bytes, bounded so a hostile peer can't spin) /write_response(fd, r). Text bodies — they cross as tycho strings, the same interior-0x00limitcore:httpnotes (fine for HTML/JSON/form APIs, not binary blobs). CRLF is built withchr(13)(tycho strings have no\rescape). -
cli— command-line argument parsing, pure string math.parse(argv) -> Cli(pass your arguments withoutargv[0]) sorts the vector into three buckets:--key=valueoptions, boolean flags (--flag, and short clusters-abc→a/b/c), and positionals (everything else, plus everything after a bare--). Values always attach with=, so the parser needs no schema of which options take a value —--verboseis unambiguously a flag. Accessors:get(c, key, default),has(c, key),flag(c, name)(long or short),positionals(c),count(c). Uses parallel arrays, not maps. -
raster— pure-Tycho raster image codecs, BMP and QOI, with no external dependency (unlikecore:image's libpng-backed PNG). AnImageis 8-bit RGBA (4 bytes/pixel, row-major, top-to-bottom); pixel data isbytes.encode_bmp/decode_bmp(writes 32-bit BGRA, bottom-up, uncompressed; reads 24- or 32-bppBI_RGB) andencode_qoi/decode_qoi(all six QOI chunk types, spec-exact 14-byte header and end marker). Both round-trip losslessly; the decoders fail closed to a 0×0 Image on a malformed / truncated / wrong-format input (checkwidth > 0). Assembling the binary output is what theto_bytes([int])builtin enables — pure Tycho otherwise can't build abyteswith an interior0x00(a black or transparent pixel is0x00components).
A core module can wrap a C library via FFI. Drop a <module>/<module>_shim.c next to
the .ty; the transpiler auto-compiles and links it on import "core:<module>" (no
--shim needed). The .ty declares the shim's functions with extern fn (and
extern "Lib" fn auto-adds -lLib for an external library; core:regex needs none —
POSIX regex is in libc). Opaque handles cross as ptr (carried by value, never
dereferenced or arena-managed — null / is_null). Both transpilers link a module's shim,
so tychoc, tychoc0 --bundle, and standalone tychoc0 all build C-shim modules.
A shim that needs a system library beyond libc — one with headers to find and a link
flag that varies by platform — declares it in a corelib/<module>/deps manifest:
pkg-config package names, one per line (# comments and blank lines ignored). For
example corelib/http/deps is just libcurl.
The deps-backed modules are http (libcurl) and crypto (libcrypto), documented above,
plus three more:
-
compress— gzip (RFC 1952) compress/decompress over zlib (deps: zlib);bytes -> bytes, fail-closed on corrupt or truncated input. -
image— PNG decode/encode over libpng (deps: libpng):decode(bytes) -> Image{width, height, pixels}(8-bit RGBA) andencode(Image) -> bytes; fail-closed on a non-PNG. JPEG is a demand-gated follow-up. -
tls— a TLS 1.2/1.3 client over OpenSSL (deps: openssl). Secure by default:connect(host, port)verifies the certificate against the system CA store, checks the hostname, and sends SNI; failure returns a null handle (never a silent insecure connection).write/read/close_connover the encrypted stream. -
Build: when
tychocauto-discovers a module's shim it reads the siblingdepsand runspkg-config --cflags --libs <name>for each, splicing the result onto the cc line (add_pkg_depsinsrc/tychoc.c). pkg-config resolves the right include path and libs per platform, so#include <curl/curl.h>+-lcurljust work without hardcoding paths. -
Skip, don't fail:
corelib/run.shprobes the samedepswithpkg-config --exists; if any dependency is missing it skips that module's test (printingskip <name> (missing dependency: …)) instead of failing, so machines without the library still build. When present, it passes the same--cflags --libson the tychoc0 link paths.
This keeps the test suite libc-only and portable while still allowing library-backed modules.
A libc-only shim (core:regex) needs no deps; a library with no .pc file can still use
extern "Lib" fn for a bare -lLib.
make corelib (→ corelib/run.sh): every corelib/test/<name>/main.ty is built three
ways — by the C transpiler, via tychoc --bundle | tychoc0, and via standalone tychoc0
(which resolves core: itself) — and all three must produce the golden
corelib/test/<name>.out. Re-record goldens with RECORD=1 sh corelib/run.sh. Part of
make ci.
Every module also has a small, readable usage example at
examples/corelib/<name>/main.ty — usage as documentation (idiomatic calls, human-friendly
output), as opposed to the assertion-style tests above. make corelib-examples
(→ examples/corelib/run.sh) validates them the same three ways against
examples/corelib/<name>.out, with the same dependency skip, and is part of make ci.
The reader docs here are mirrored from /docs in the repo — the canonical source. · tycho-lang.com · MIT licensed
- Tutorial
- The Tycho thesis: value semantics makes implicit arenas work
- Architecture & project status
- From
mallocto implicit arenas
- Tycho language reference
- Basics
- Types
- Functions and closures
- Structs and tuples
- Enums, options, and
match - Arrays and slices
- Subscripts (user-defined projections)
- Maps
- Generics
- Concurrency
- FFI (calling C)
- Packages
- Builtins
- Memory model in the self-hosted compiler
- Tycho aggregates: arrays & structs under value semantics
- Maps with arbitrary value types
m[k]as a place: in-place map-value mutation- Generics (monomorphized)
- Concurrency in Tycho
- FFI — calling C from Tycho
- Packages & modules
- corelib — Tycho's standard library
- Performance: the self-hosted transpiler vs the C one
- Debugging generated C