# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-08-04

### Changed

- Upgraded `ichor` (dev-only) `0.2.1` -> `0.3.0` and `ichor_runtime`
  (runtime) `0.1.0` -> `0.2.0`; both `mix.exs` constraints now
  major.minor only (`~> 0.3` / `~> 0.2`), no patch pin, per this
  project's own dependency-version convention. `ichor` `0.3.0` is what
  unblocks the `ichor_runtime` bump -- `0.2.1` still internally pinned
  `ichor_runtime ~> 0.1.0`, capping resolution below `0.2.0` regardless
  of Logos's own requirement. `lib/logos/reader/generated.ex`
  regenerated via `mix ichor.gen` against the new `ichor` -- a large
  textual diff (internal helper functions renumbered by the newer
  codegen) but no behavioral change: `priv/grammar/logos.aether` itself
  is untouched, `Grammar.VM.Token`'s shape and `Logos.Reader.tokenize/1`'s
  output are unchanged (spot-checked directly), and the full test suite
  passes unchanged.

### Added

- **Generated stdlib, special-forms & primitives doc reference**
  (`guides/language/stdlib/*.md`, new "Stdlib Reference" ExDoc group),
  built by `Logos.StdlibDocs.documents/0` (`mix logos.gen_docs`) -- an
  overview/table-of-contents page, a special-forms page, a primitives
  page (every Layer-1 `Logos.Primitives` entry, each now with a real
  docstring instead of the old generic `"Layer-1 primitive"`
  placeholder -- `(doc +)` etc. finally return something meaningful),
  and one page per stdlib namespace, each a flat, per-symbol lookup
  pulled live from that item's own docstring, companion to
  `guides/language/LOGOS.md`'s narrative reference. Every documented
  item (special form, primitive, function, or macro) also gets a
  runnable example: actually evaluated at generation time, in its own
  isolated process (so one example's stray process state -- a leftover
  mailbox message, a changed process flag -- can never leak into
  another's result), never a hand-typed "expected output" that could
  drift from what the code really does. Every stdlib-namespace entry
  (function/macro/var, across all eight `priv/stdlib/*.logos`
  namespaces) also shows its own defining form's exact source,
  reconstructed byte-for-byte from `Logos.Reader.tokenize/1`'s
  position-preserving token stream. `mix test` fails if any checked-in
  page drifts from a fresh regeneration, if the hand-maintained
  special-forms list drifts from `Logos.Eval`'s actual clauses, or if
  any documented item is missing an example (or has one that errors).
- `import` accepts an optional trailing docstring argument, same shape
  as `def`'s own 3-arg form, attached as `:doc` metadata on the interned
  Var -- closes the gap where a raw imported name used with no
  Logos-level wrapper (`logos.string`'s own `trim`/`reverse`/
  `capitalize`/`replace`/`split`) had no docstring reachable via
  `(doc name)`.
- Docstrings for previously-undocumented public stdlib vars: all twelve
  `logos.string` functions (five raw-imported, seven wrapped), seven
  `logos.seq` `-onto` accumulator helpers, and `logos.multimethod`'s
  `multimethod-registry`.

## [0.1.0] - 2026-08-02

Initial release.

### Added

- **Three new stdlib namespaces** (`logos.set`, `logos.walk`,
  `logos.string`) plus `read-string`/`pr-str` in `logos.core` -- closing
  out the "are there more stdlib modules worth adding" follow-up to the
  missing-functionality audit below. All three new namespaces are
  loaded into every `Runtime` but deliberately **not** auto-referred
  into `logos.core` (`require` them explicitly), matching real Clojure
  exactly: `clojure.set`/`clojure.walk`/`clojure.string` are all
  separate, explicitly-required namespaces there too, unlike
  `clojure.core`'s own multimethod/protocol machinery (why
  `logos.multimethod` *is* auto-referred).
  - **`logos.set`** -- `union`/`intersection`/`difference`/`subset?`/
    `superset?`/`select`/`map-invert`/`rename-keys`, pure Logos over
    existing primitives, no interpreter changes. No `project`/`rename`/
    `index`/`join` (real Clojure's relational-algebra corner of
    `clojure.set`) -- a real, deliberate scope trim, genuinely niche
    outside actual in-memory relational querying.
  - **`logos.walk`** -- `walk`/`postwalk`/`prewalk`, a direct port of
    `clojure.walk`. Rebuilding a map or sorted collection always
    produces a plain map/set -- a real, deliberate scope trim (Logos has
    no generic `empty` constructor to preserve an arbitrary collection's
    own shape with, same reason `select-keys` already accepts this).
  - **`logos.string`** -- Clojure-idiomatic string manipulation
    (`upper-case`/`lower-case`/`capitalize`/`triml`/`trimr`/`includes?`/
    `starts-with?`/`ends-with?`/`blank?`/`join`/`split-lines`/`replace`/
    `reverse`), every function a thin wrapper over an `import`ed,
    allowlisted Elixir `String.*` function -- going through the *same*
    sandboxing chokepoint `import` itself uses, never a direct,
    allowlist-bypassing primitive. A deliberate, explicit decision (Jan
    chose this over leaving string manipulation as purely the host
    embedding app's job, the alternative presented): `Logos.Interop.Allowlist`
    grew seven new `String.*` entries specifically to support it, and
    its own moduledoc was updated to say so -- it no longer frames itself
    as just a minimal test-fixture seed list. **`join`/`split-lines`/
    `blank?` need no interop at all**, pure Logos over what's already
    available. `split`/`replace` only ever take a literal string
    pattern, never a regex (Logos has no regex literal syntax to
    construct one from); `replace-first`/`index-of`/`last-index-of`/
    `trim-newline`/`escape` skipped as genuinely niche. **This
    namespace's own `reverse` (string reversal) collides by name with
    `logos.seq`'s `reverse` (list reversal) if `:refer [:all]`'d** --
    documented prominently (this file's own header comment, `LOGOS.md`
    7.12, a dedicated test) as the exact same footgun real `(require
    '[clojure.string :refer :all])` has in real Clojure too, which is
    why `(require '[logos.string :as str])` is the documented,
    recommended form.
  - **`read-string`/`pr-str`** (`logos.core` primitives) -- `read-string`
    parses a string as Logos source and returns the first form as plain,
    unevaluated data (`Logos.Reader.read/2`, reachable from Logos code
    now); malformed input raises a catchable `:read-error`. `pr-str` is
    `str`'s round-trippable sibling -- every argument (strings included)
    goes through `Logos.Printer.print/1` unchanged, unlike `str`, which
    special-cases strings to pass through bare.

  Found and fixed two real bugs while building this, both sharper
  versions of the `^:private`-cross-namespace hazard `not-found-sentinel`
  already surfaced during the missing-functionality work below --
  **being public isn't sufficient for a namespace that isn't
  auto-referred into `logos.core`**: `logos.set`'s `superset?` called
  `subset?` via a bare reference, and `logos.walk`'s `postwalk`/
  `prewalk` called `walk` (and themselves, recursively) the same way;
  most of `logos.string`'s own wrappers had the identical shape (calling
  their own raw imported name). Each broke with `{:unbound_symbol, ...}`
  the moment a caller used `(require '[logos.set :as set])` (no
  `:refer`) instead of `:refer [:all]` -- exactly the *recommended* form
  for `logos.string`, specifically to dodge the `reverse` collision
  above, which would otherwise have made the two pieces of guidance
  directly contradict each other. Caught by actual `mix run` smoke
  tests (not just the example-based test suite) before any of it reached
  a committed test file; fixed by qualifying every same-namespace
  internal cross-call with its full `logos.set/...`/`logos.walk/...`/
  `logos.string/...` prefix, which -- unlike a bare reference -- always
  resolves against the literal target namespace regardless of caller
  context (the same mechanism `logos.core/build-let` already relies on
  to stay reachable despite being `^:private`). Each of the three new
  files' own header comments now document this explicitly, and
  `CONTRIBUTING.md`'s existing `^:private` guidance grew a matching
  section for it.

  Also found and fixed a real, previously under-documented gap while
  writing `logos.walk`'s own tests: **a Logos vector/map/set literal
  never evaluates its own elements**, a genuine, deliberate divergence
  from Clojure (`[1 (+ 1 1)]` evaluates to itself verbatim in Logos, a
  two-element vector whose second element is the literal, unevaluated
  list `(+ 1 1)` -- not `[1 2]`), already correctly implemented and
  commented in `Logos.Eval.eval/3` but never stated anywhere in
  `guides/language/LOGOS.md` at the level such an easy-to-miss semantic
  deserves (only a narrower, destructuring-scoped mention existed).
  Added a dedicated, prominent note in `LOGOS.md` section 2 and the
  cheatsheet's reader-syntax table -- caught only because an actual
  `mix run` smoke test of a naively-written `postwalk` test hit it
  directly.

- **A batch of missing map/seq functionality**, found by a full audit of
  the stdlib layer for missing functionality, optimization
  opportunities, and Elixir code that could move to Logos (see this
  file's own "Changed" entries below for the other two). Two genuine
  primitive extensions, the rest pure Logos over them:
  - `get`/`assoc` now support **vectors** (index-based, like real
    Clojure) -- previously unsupported entirely. `assoc` allows an
    existing index or exactly one past the end (append, matching `conj`
    and real Clojure); anything further raises `:index_out_of_bounds`
    rather than silently leaving a gap (the underlying `:array.set/3`
    auto-extends past its bound, which `Logos.Vector` deliberately
    guards against).
  - `get` (and keyword-as-function, `(:k coll)`) now support **sets**
    (real Clojure's own membership-as-lookup: the element itself if
    present, `default`/`nil` otherwise).
  - `contains?`, `keys`, `vals` -- the item `ROADMAP.md` flagged but
    deliberately left untracked while closing out transients/sorted
    collections. `contains?` is a pure Logos sentinel-default wrapper
    over `get` (no primitive of its own needed, now that `get` dispatches
    correctly across every shape); `keys`/`vals` are thin `to-list`
    wrappers, map/sorted-map-only (throws on anything else, matching
    real Clojure).
  - `merge`/`merge-with` -- seeded from the first non-`nil` map itself
    (not always a fresh `{}`), then `into`s the rest, so merging into a
    sorted-map keeps it sorted (matching real Clojure's own `conj`-based
    `merge`).
  - `get-in`/`assoc-in`/`update`/`update-in` -- nested map ergonomics.
    `get-in` uses its own internal-only sentinel (`get-in-miss`,
    distinct from `contains?`'s `not-found-sentinel`) to detect a
    missing intermediate step without confusing it with a caller-
    supplied `not-found` value that might otherwise collide with a real
    stored one.
  - `select-keys` -- always returns a plain map even from a sorted
    source, a real, minor, deliberate divergence from Clojure (not worth
    a dedicated sorted-preserving path for this one function).
  - `take-while`/`drop-while`, `partition`/`partition-all`/
    `partition-by`, `flatten`, `zipmap`/`mapcat`/`interpose`/
    `interleave`, `juxt`/`every-pred`/`some-fn`. `partition`/
    `partition-all` reject a non-positive `n`/`step` (throws
    `:invalid-partition-size`) rather than looping forever -- this layer
    is eager, not Clojure's lazy version, so it can't just never fully
    realize an infinite result the way Clojure's own `(partition 0
    coll)` effectively doesn't. No 4-arity padding form
    (`partition`/`n`/`step`/`pad`) and no multi-collection `map`/
    `mapcat` (Clojure's own are variadic) -- deliberate scope trims,
    same spirit as this codebase's other trimmed edge variants;
    `interleave` is the one multi-collection exception, since a
    single-collection `interleave` is degenerate and its own
    implementation doesn't actually need a variadic `map` (it maps
    `first`/`rest` over the *outer* list of collections, itself always
    one collection).

  Found and fixed one real bug while building this: `not-found-sentinel`/
  `get-in-miss` (`priv/stdlib/seq.logos`) were first written `^:private`
  -- the exact hazard this file's own header comment already documents
  (a private helper is only reachable via the "direct var in the current
  namespace" check, never through `logos.core`'s refer of `logos.seq`,
  which DOES filter by `public?`) -- breaking `contains?`/`merge-with`/
  `select-keys`/`get-in` for every caller outside `logos.seq` itself,
  i.e. always in practice. Caught by an actual `mix run` smoke test
  before it reached a committed test file; fixed by making both public,
  matching `test-registry`/`multimethod-registry`'s own precedent for
  this exact situation.

### Changed

- **`logos.seq`'s `map`/`filter`/`take`/`count`/`range`/`repeat` are now
  genuinely tail-recursive.** Found during a full audit of the stdlib
  layer (missing functionality / optimization opportunities / Elixir
  code that could move to Logos): each of these six built its result by
  wrapping its own recursive call in `cons`/`+` (e.g. `(cons (f (first
  items)) (map f (rest items)))`), which is *not* tail position -- unlike
  `reverse-onto`/`distinct-onto`/`reduce`/`drop` (already correct,
  accumulate-then-`reverse`, elsewhere in the same file), so none of
  these six ever actually benefited from this project's own headline TCO
  feature; recursion depth scaled with input size. Rewritten to the same
  accumulate-then-`reverse` pattern as their already-correct neighbors
  (`count-onto`/`take-onto`/`map-onto`/`filter-onto`/`range-onto`/
  `repeat-onto`, public per this file's own established `^:private`-is-
  unsafe-for-a-cross-namespace-callee rule). Purely internal -- identical
  observable output confirmed by the existing test suite (`stdlib_test.exs`,
  `seq_property_test.exs`'s property coverage, `logos_scripts_test.exs`)
  with zero changes needed to any of it.
- `test.logos`'s own header comment had the same stale "`try`/`catch`
  only ever catches an explicit `(throw ...)`" claim already found and
  fixed in `guides/language/LOGOS.md` while building the item below --
  missed there the first time; fixed the same way.

### Added

- **Transients** and **persistent sorted collections**
  (`sorted-map`/`sorted-set`), `ROADMAP.md`'s last open Tier 3 item --
  closing out the roadmap's own tracked gap list entirely (only a
  pre-existing, never-tracked gap, `keys`/`vals`/`contains?`, noticed
  while building this but out of scope for it, remains).

  **Sorted collections**: genuine new `Logos.SortedMap`/`Logos.SortedSet`
  runtime value types (mirroring `Logos.Vector`'s own precedent), backed
  by a sorted association list -- the same "boring, obviously correct,
  O(n) per op" tradeoff `sort`/`distinct` already made, not a balanced
  tree, since script-sized collections need correctness far more than
  asymptotics. `sorted-map`/`sorted-set` order by a new `compare`
  primitive (Clojure's own general 3-way ordering -- numbers across the
  full tower, strings, chars, keywords/symbols by `{ns name}`,
  vectors/lists elementwise -- returning -1/0/1, distinct from `<`/`<=`/
  ..., which stay strictly numeric, matching real Clojure exactly);
  `sorted-map-by`/`sorted-set-by` take an explicit comparator instead --
  an ordinary Logos function called back through the existing
  `Logos.Eval.apply_fn/3` (the same mechanism `apply` itself already
  uses), no new evaluator machinery. `type-of` returns `:sorted-map`/
  `:sorted-set`; `map?`/`set?` (`core.logos`) were widened to recognize
  both their plain and sorted variant, so a sorted collection is a
  `map?`/`set?` like real Clojure's is; a new `sorted?` predicate checks
  specifically. `get`/`assoc`/`dissoc`/keyword-as-function all work on a
  sorted-map; `conj`/`disj`/`into` (`logos.seq`) all keep a sorted-set
  sorted rather than silently demoting it to a plain set on the first
  mutation -- the bug this would otherwise have been is why `into`'s set
  branch now reuses `conj` instead of its own separate `list->set`
  rebuild. `=` treats a sorted collection as content-equal to a plain one
  with the same entries (map/set equality is by content, never by
  concrete representation, matching real Clojure: `(= (sorted-map :a 1)
  {:a 1})` is `true` there too). No dedicated reader syntax and no
  round-trip -- printing produces ordinary `{...}`/`#{...}` text (in
  sorted order), which reads back as an ordinary map/set, matching real
  Clojure there as well (`(pr-str (sorted-map :a 1))` reads back as a
  plain hash-map too). Added a real new `disj` (`logos.seq`) alongside
  this -- it didn't exist at all before, and both `sorted-set` and
  transient sets need a removal counterpart to `conj`.

  Also used `compare` to fix a real, previously-undocumented gap: `sort`/
  `sort-by` (`logos.seq`) used to compare via `<=`, which is strictly
  numeric (matching real Clojure's own `<=`, which also throws on two
  strings) -- so `(sort ["b" "a"])` used to error. Both now sort via
  `compare` instead, closing that gap for free.

  **Transients**: `transient`/`conj!`/`assoc!`/`dissoc!`/`disj!`/`pop!`/
  `persistent!`, scoped to vectors/maps/sets only (not lists, not sorted
  collections -- neither is an "editable collection" in real Clojure
  either, so `(transient (sorted-map))`/`(transient '(1 2))` throw here
  too). Planned explicitly first (the two representations on the table --
  a process+message design mirroring `atom`, vs. a `:private`-ETS-backed
  value type -- were presented with a concrete recommendation before any
  code); chose the latter (`Logos.Transient`, a new value module) on
  explicit request. Deliberately *not* built the way `atom` is: an atom
  exists to be a safely *shared* mutable reference (hence process +
  message-passing, atomicity free from BEAM's one-message-at-a-time
  guarantee), but a transient exists to be the opposite -- real Clojure
  transients are documented single-thread-use only, and a `:private` ETS
  table makes that a genuine BEAM-enforced guarantee (any process but the
  creator gets `ArgumentError` touching it) rather than a documented-only
  discipline, while also making every mutation real O(1) in-place work
  (no message round-trip) -- actually delivering the performance
  transients exist for, which the process-based alternative would not
  have. `persistent!` deletes the backing table after extracting the
  final value, so a transient used afterward (`persistent!` a second
  time, or any mutator) raises the same `ArgumentError` under the hood,
  surfaced as a single, uniform, catchable
  `:transient-used-after-persistent` failure -- one mechanism naturally
  covering both of Clojure's transient safety rules, not two separate
  checks. `conj!`/`assoc!`/... always return the transient itself (not
  the new value), matching Clojure's own "always use the returned value"
  contract. `get`/`count`/`to-list` (and everything `logos.seq` builds on
  `to-list`, e.g. `nth`/`empty?`) work directly against a *live*
  transient with no `persistent!` needed first, for free, since
  `to_list_value/1`'s own dispatch (`Logos.Primitives`) just peeks the
  transient's current value -- matching real Clojure transients
  implementing the same read interfaces their persistent counterparts do.

  Found and fixed one real, adjacent doc bug while writing this:
  `guides/language/LOGOS.md`'s `logos.test` section still claimed
  `try`/`catch` "only ever catches an explicit `(throw ...)`" -- stale
  since the broader `try`/`catch` work earlier in this file; fixed to
  explain the real, current reason `deftest` still needs process
  isolation (no implicit `try`/`catch` wraps a test body at all, not
  "primitive failures aren't catchable").
- **Reader tagged literals** (`#tag value`), `ROADMAP.md`'s second Tier
  3 item. Jan chose the full, genuinely extensible route (a per-
  `Runtime` registry, `#inst`/`#uuid` as two pre-installed entries in
  it rather than special-cased, plus a new `register-data-reader!`
  primitive) over a narrower built-in-only version, matching real
  Clojure's own `*data-readers*` design. Reader conditionals
  (`#?(:clj ...)`) are explicitly **not** included -- `ROADMAP.md`'s
  own reasoning (Logos has exactly one target platform, itself) still
  holds, nothing found while researching this changed that.

  Resolved at **read time**, not eval time -- `` '#inst "..." `` still
  yields the resolved value under `quote`, matching every other
  literal, not sugar for a deferred function call. This is the one
  place `priv/grammar/logos.aether`/`Logos.Reader.Actions` genuinely
  needs `Logos.Runtime` state *during* the parse itself (not just at
  the later syntax-quote desugaring pass, which is why `runtime` was
  already an optional parameter): `Logos.Reader.read/2`/`read_all/2`
  now thread it through as `Ichor.Actions`' own `context`, previously
  hardcoded to `nil`. A registered reader is always a plain Elixir
  function reached through `Logos.Interop.Allowlist` -- the exact same
  sandboxing chokepoint `import` already uses -- **never an arbitrary
  Logos closure**, so `handle_rule(:tagged_literal, ...)` calls it
  directly via `Kernel.apply/3` with no `Logos.Eval` involved at all,
  keeping `Logos.Reader.Actions`'s own "pure reification only"
  architectural rule intact (this is the same category of thing
  `Char.parse/1` already is, not a new coupling to the evaluator). A
  deliberate narrowing from the literal "Logos code can register its
  own tags" framing this item started from -- flagged and confirmed
  before writing any code.

  `#inst`/`#uuid` (`Logos.DataReaders`, two new `Logos.Interop.Allowlist`
  entries) validate their string argument (real `DateTime.from_iso8601/1`
  for `#inst`, an 8-4-4-4-12 hex-digit shape for `#uuid`) but return it
  **unchanged**, as a plain Logos string -- deliberately no new
  `Logos.Instant`/`Logos.Uuid` value type (a separate,
  `Logos.Record`-scale design question this item didn't ask for). Direct,
  documented consequence: unlike Clojure's own `#inst`/`#uuid`, these two
  don't round-trip through `Logos.Printer.print/1`/`Logos.Reader.read/1`
  back to `#inst "..."`/`#uuid "..."` -- they print as ordinary quoted
  strings, since a plain string carries no record of which tag (if any)
  produced it. Also found and documented (not a bug, an inherent
  consequence of `Logos.eval_string_sequence/3`'s already-existing
  "read every top-level form up front, before evaluating any of them"
  behavior, the same one mid-sequence `(ns ...)`/`(in-ns ...)` already
  has): `register-data-reader!` and a use of that same new tag can't
  appear in the same top-level source string/script file -- only in a
  later, separate `eval_string`/`eval_string_sequence` call.

  Found and fixed a real bug in `mix logos.format`/`Logos.Format` while
  building this: the new bare `#` prefix token needed teaching the
  formatter "no space before the next token" (`#inst`, not `# inst`).
  The first attempt did this with a blind text check ("does the
  accumulated output so far end in `#`?"), which also matched an
  ordinary symbol whose own text just happens to end in `#` (auto-
  gensym syntax, `g#`/`x#`) -- gluing it to whatever token followed with
  no space. Caught by re-running the formatter against real stdlib
  source (`priv/stdlib/core.logos`'s `or` macro, `priv/stdlib/test.logos`'s
  `assert`/`assert-throws`) and diffing the result before trusting it,
  not just a synthetic test -- corrupted output had already merged
  `g# g#`/`v# v#`/`a# e#` into single unparseable tokens on disk (never
  committed; restored from git before re-fixing). Fixed by wiring up
  `Logos.Format`'s already-threaded-but-unused `suppress`/`@prefix`
  machinery (tracking the *actual previous token's identity*, not a
  guess from accumulated text) instead of extending the text heuristic
  further -- see `test/logos/format_test.exs`'s new regression tests.
- **`try`/`catch` now catches primitive-level failures** (division by
  zero, an unbound symbol, a wrong-arity call, ...), not only an
  explicit `(throw ...)` -- `ROADMAP.md`'s first Tier 3 item, previously
  a deliberate, documented divergence from Clojure; revisited and built
  on explicit request after weighing the two implementation routes (a
  surgical widening of `try` alone, vs. a full internal switch to real
  Elixir `raise`/`rescue`) -- went with the latter. `Logos.Eval`/
  `Logos.Primitives`' entire `{:ok, value} | {:error, reason}` return
  convention is now raise-based internally: every primitive-level
  failure raises a new `Logos.EvalError` (`reason` unchanged from what
  the old tuple's second element always was), modeled directly on
  `Logos.Macroexpand`'s pre-existing `Logos.MacroError` pattern. `try`'s
  own `rescue` (already there for `Logos.Thrown`) now also catches
  `Logos.EvalError`, matching a `catch` clause against a tag *derived*
  from `reason` (a bare atom becomes its own hyphenated keyword,
  `:division_by_zero` -> `:division-by-zero`; a tuple's first element the
  same way) -- a catch clause literally tagged `:error` is an additional
  wildcard, matching any primitive-level failure, mirroring Clojure's
  `(catch Exception e ...)`; an explicit `(throw ...)`'s own matching
  stays exact-tag-only, unchanged. The caught value is the failure's own
  human-readable string message, not an attempt at structured Elixir-
  term-to-Logos-value conversion. The public API
  (`Logos.eval_string/3`/`eval_string_sequence/3`) is contractually
  unaffected -- still `{:ok, value, env} | {:error, reason}`, `reason`
  the exact same shape as always; this is purely an internal propagation
  mechanism change. Verified TCO wasn't broken by the switch (no new
  `try`/`rescue` added anywhere on the hot recursive eval path --
  `test/logos/tco_test.exs`'s 10,000,000-iteration test re-run
  specifically to confirm).
- **Records and protocols** (`defrecord`/`defprotocol`/`extend-type`),
  `ROADMAP.md`'s last open Tier 2 item, deliberately deferred from the
  multimethods work because it raises a question multimethods never
  had to answer: what does a user-defined Logos type/instance actually
  look like? Two routes were on the table (a plain map with a
  conventional type-tag key, vs. a genuine new runtime value); asked
  the user directly, who chose the latter for real opacity and closer
  parity with actual Clojure record semantics. New `Logos.Record`
  value module (`lib/logos/value/record.ex`), the first new core
  value type this session and the first item requiring real
  interpreter-level integration rather than only primitives/macros
  around the existing model: `type-of` (`Logos.Primitives`) returns a
  record's own namespace-qualified tag directly, never the generic
  `:map` tag a plain `{...}` literal gets; `get`/`assoc` get record-
  aware clauses (`assoc` returns a new record of the *same* type,
  never demoting to a plain map -- `dissoc` on a record is
  deliberately unsupported, real Clojure's demote-to-map behavior
  being genuine extra complexity out of scope for this first pass);
  keyword-as-function (`Logos.Eval.apply_fn/3`) now accepts a record
  alongside a map/`nil`; `Logos.Printer.print/1` prints
  `#type-kw{...}`, runtime-only like `Fn`/`Atom`/`Pid`. Two small new
  primitives back it: `record` (Elixir-side of necessity, building an
  arbitrary struct isn't expressible in pure Logos, the same
  justification `pid->atom` already established) and `current-ns`
  (exposes `Logos.Runtime.current_ns/1` to Lisp code, needed so
  `defrecord` can fix a record type's tag to the namespace it was
  *invoked* from, captured once at macro-expansion time, independent
  of whoever later calls the constructor -- also added a `symbol`
  primitive, `keyword`'s counterpart, to let `defrecord` synthesize
  its `->Name`/`Name?` var names). `defrecord`/`defprotocol`/
  `extend-type` themselves are pure Logos (`priv/stdlib/core.logos`,
  `priv/stdlib/multimethod.logos`): `defprotocol`/`extend-type` are
  thin sugar generating `defmulti`/`defmethod` calls dispatching on
  `type-of`, which is now a uniform dispatch key across both record
  types and every built-in type (`extend-type :vector Shape ...` and
  `extend-type Point Shape ...` work identically). No
  `satisfies?`/`extends?` introspection and no protocol-name registry
  -- deliberately minimal, the same scope trim multimethods' own
  missing hierarchy support already established.

  Found and fixed a real bug while building this: `defrecord`'s first
  draft referenced its own type-name symbol *bare* inside the
  generated constructor/predicate bodies, instead of the already-
  computed tag value -- a bare reference resolves against the
  CALLER's current namespace at the moment that code actually runs
  (documented in `core.logos`'s own header comment), not the
  namespace `defrecord` was invoked from, so a qualified constructor
  called from a namespace with its own same-named record silently
  picked up the WRONG type. Caught by an actual cross-namespace
  `mix run` smoke test before it ever reached a test file; fixed by
  splicing the literal tag value directly instead of the symbol
  reference. A second, narrower bug: `defprotocol`/`extend-type`'s own
  helper functions (`defprotocol-multis`/`extend-type-methods`/
  `protocol-dispatch`) were first written `^:private`, which broke
  them for every caller namespace other than `logos.multimethod`
  itself -- the exact hazard `core.logos`'s header comment already
  documents for helpers living outside `logos.core`'s own auto-refer
  exemption; fixed by making them public, matching
  `register-multimethod!`/`register-method!`'s own precedent in the
  same file.
- **Dynamic vars and `binding`**, `ROADMAP.md`'s third Tier 2 item.
  Unlike every other item shipped this session, this one genuinely
  couldn't be pure Logos over existing primitives: dynamically-scoped
  symbol resolution has to be intercepted at the point `Logos.Eval`
  resolves a Var's value, which is Elixir-side. A Var `def`'d with new
  `^:dynamic` meta (`Logos.Eval`'s private `meta_from_symbol/2`,
  alongside the existing `^:private`/`^:macro` flags) can be thread-locally (i.e.
  per-BEAM-process) rebound for the extent of a `binding` body via two
  new primitives, `push-thread-binding!`/`pop-thread-binding!`
  (`Logos.Primitives`), storing each override on the calling process's
  own **process dictionary** -- deliberately not another `{tag, pid}`
  row in `Logos.Runtime`'s shared `:public` ETS table (the pattern
  `current_ns` already uses): dynamic bindings need to be invisible to
  *other* processes, the opposite of why that table is `:public`, and
  the process dictionary comes with a real win the `current_ns` design
  explicitly doesn't have (its own moduledoc calls out `{current_ns,
  pid}` rows never being cleaned up on process exit as an accepted
  tradeoff) -- a process dictionary just vanishes with its process, no
  leak to accept. The read side is one new shared private helper in
  `Logos.Eval`, `get_var_value_dynamic_aware/3`, called from both bare-
  and qualified-symbol resolution in place of a direct
  `Namespace.get_var_value/3` call: a Var that was never `binding`-bound
  (the overwhelming majority) is completely unaffected. `binding` itself
  is a pure `priv/stdlib/core.logos` macro over the existing `try`/
  `finally` special form -- no new special form, matching how
  `if`/`case`/`condp` are all built on `cond`. Nested `binding` shadows
  correctly (each override is a stack); a spawned child process sees the
  Var's root value, never its parent's active binding (a fresh process
  dictionary), matching real Clojure (a bare new thread doesn't inherit
  dynamic bindings either, only `bound-fn` does). Deliberately out of
  scope: rebinding the innermost active binding from within its own
  `binding` scope (Clojure's `set!`).
- **`case`/`condp`**, `ROADMAP.md`'s second Tier 2 item -- pure macros
  over `cond`/`if`, no interpreter changes, in `priv/stdlib/core.logos`.
  `case`'s test values are literal/unevaluated (a test may also be a
  list of alternatives, e.g. `(1 2 3)`, matched via a generated `or`);
  `condp`'s test values ARE evaluated expressions, tried per clause as
  `(pred test expr)`. Both evaluate the dispatched-on expression exactly
  once (regardless of how many clauses are tried), support a trailing
  unpaired default form, and throw `:no-matching-clause` when nothing
  matches and no default was given. `condp`'s `:>>` result-fn form is
  explicitly out of scope. Hit the same gensym cross-scope bug the
  `cond->`/`cond->>`/`some->` macros already needed fixing for earlier
  (a manually-quoted `'g#` outside a syntax-quote does *not* refer to
  the same symbol as `g#`'s auto-gensym *inside* it) -- fixed the same
  way, by calling the `gensym` primitive directly and splicing the one
  resulting value everywhere it's needed.
- **Multimethods** (`defmulti`/`defmethod`), `ROADMAP.md`'s first
  Tier 2 item -- ad-hoc polymorphism, dispatching on the result of an
  arbitrary "dispatch function" called with a multimethod's own
  arguments (strictly more general than single-dispatch-on-first-
  argument-type protocols). New file, `priv/stdlib/multimethod.logos`
  -> `logos.multimethod`, referred into `logos.core` like `logos.seq`/
  `logos.map`/`logos.concurrency`. Real Clojure semantics: `:default`
  fallback, methods registered after a multimethod's first call take
  effect immediately (one shared, mutable dispatch table, not a fixed
  snapshot), `:no-method-for-dispatch-value` thrown when nothing
  matches. No hierarchy support (`isa?`/`derive`/`prefer-method`) or
  `remove-method`/`methods`/`get-method` introspection -- deliberately
  out of scope for a first pass. Protocols/records (`defprotocol`/
  `deftype`/`defrecord`) are explicitly **not** included -- they raise a
  separate design question (what does a user-defined Logos type even
  look like?) that multimethods don't, and are deferred to their own
  `ROADMAP.md` item; the registry design here (a plain, shared,
  `intern-var!`-mutated map, not an atom) reuses `logos.test`'s own
  already-proven pattern rather than inventing a new one. Found and
  fixed two real bugs while building this -- one a genuine language-level
  gap, one specific to this feature's own first draft -- see "Fixed"
  below.
- **Destructuring in `let`/`defn`/`defn-`**, `ROADMAP.md`'s fifth tier-1
  item and the last one: a binding/param position accepts a vector
  pattern (`[a b]` positional, `[a b & more]` rest, `[a b :as whole]`
  the original value too) or a map pattern (`{:keys [a b]}`, explicit
  `{name :key}` pairs, `{:keys [a] :as m}`), nested patterns included --
  `priv/stdlib/core.logos`, real Clojure semantics, one new primitive
  (`keyword`, needed to turn a `{:keys [a]}` pattern's binding name into
  the keyword `:a` to `get` it by, with no other way to ask "what string
  is this symbol's own name" from pure Logos). **`fn` itself does not
  support this** -- only `let`/`defn`/`defn-` (all macros) do, expanding
  each pattern into a fresh `gensym`'d plain-symbol name before handing
  the real `fn` special form anything, the same layering real Clojure's
  own `fn` macro uses over `fn*`. Does not support `:strs`/`:syms`
  (string-/symbol-keyed map lookup) or `:or` (default values) -- noted
  as a possible follow-up in `ROADMAP.md` if either turns out to matter.
  A compound pattern's value-form is evaluated exactly once, no matter
  how many parts get extracted from it (bound to a `gensym`'d temp
  first, verified with a side-effecting value-form, not just inferred
  from the expansion).

  The destructuring helpers themselves (and `build-let`'s own updated
  body) are written using *only* Layer-1 primitives plus `cond`/`fn` --
  never `let`, never a `logos.seq` function (`map`/`reduce`/`reverse`/
  `empty?`/`nth`/...). This is a hard bootstrap constraint, not a style
  choice: `defn`'s own macro body now calls into this machinery on
  every `defn` it expands, including the very first one in
  `core.logos` (the type-predicates section), which runs long before
  `logos.seq` loads -- exactly the same class of bug already found once
  this session in `defn`'s multi-arity support (`vector?` not existing
  yet). Caught here too, the same way, before it reached a test file.
- **A real seq abstraction over every collection**, `ROADMAP.md`'s
  fourth tier-1 item: every `logos.seq` function that takes a `coll`
  argument (`map`/`filter`/`reduce`/`take`/`drop`/`reverse`/`count`/
  `empty?`, plus everything new below) now accepts a list, vector, map,
  set, or `nil`, coercing via the `to-list` primitive -- not just lists
  as before. Chose eager coercion over a lazy-seq abstraction (see
  `ROADMAP.md`'s own note on the two designs): `to-list` on an
  already-a-list value is an O(1) pattern match (`Logos.Primitives`'s
  `to_list_value/1`, returns the same list reference, no rebuild), so
  coercing on every recursive step costs nothing real, and every
  function's own result stays a plain list regardless of input shape --
  matching real Clojure exactly (`(map f a-vector)` there returns a lazy
  seq, never a vector, either). Pure `priv/stdlib/seq.logos`, no new
  primitives, no interpreter changes. New functions, real Clojure
  semantics throughout: `nth` (2-arity throws `:index-out-of-bounds`
  with the *originally requested* index -- not, as an earlier draft did,
  however many recursive steps happened to be left when it ran out;
  3-arity returns a given `not-found`), `second`, `last`, `some`,
  `every?`, `distinct` (O(n^2), documented as the simple/obviously
  correct choice, not the fastest possible -- same spirit as `reduce`'s
  own moduledoc note), `sort`/`sort-by` (`<=`-based insertion sort,
  same O(n^2) tradeoff), `frequencies`, `group-by` (buckets into
  *vectors*, matching real Clojure's own shape exactly), `range`/
  `repeat` (eager and bounded -- unlike Clojure's own lazy-infinite
  forms, a `0` step throws `:invalid-range-step` rather than looping
  forever, and `repeat` has no unbounded 1-arity form), and `conj`/
  `peek`/`pop` (`conj` is `into`'s one-element-at-a-time cousin, same
  per-target-shape dispatch; `peek`/`pop` look at the front for a list,
  the back for a vector, Clojure's own two different "natural end"
  conventions for the two shapes).
- **Multi-arity `defn`/`defn-`**, `ROADMAP.md`'s third tier-1 item:
  `(defn name ([p1] b1) ([p1 p2] b2))`, same shape `fn` itself already
  supported, optionally with a leading docstring
  (`(defn name "doc" ([p1] b1) ([p1 p2] b2))`). Pure `core.logos`, no
  new primitives. Found and fixed a real bootstrap-breaking bug while
  implementing: the natural way to tell a bare params vector apart from
  an arity-clause list is `vector?`, but `vector?` itself is defined via
  `defn` later in the very same file -- `defn`'s own macro body calling
  `vector?` meant the *first* `defn` call anywhere (`(defn nil? ...)`,
  which comes before `vector?`'s own definition) failed with
  `{:unbound_symbol, "vector?"}`, breaking `Logos.Stdlib.load!/1`
  itself. Fixed by checking `(= (type-of ...) :vector)` directly instead
  -- `type-of` is a Layer-1 primitive, available from the very start,
  with no dependency on anything `defn` itself is used to build.
- **`ROADMAP.md`**: a prioritized, verified list of Clojure features
  Logos doesn't have yet (threading macros, destructuring, a uniform
  seq abstraction, basic numeric/utility stdlib, multi-arity `defn`,
  protocols/multimethods, dynamic vars, and more), distinct from
  `CONTRIBUTING.md`'s "Known gaps" (which tracks bugs/incompleteness in
  behavior already claimed to work, currently empty).
- **Threading macros**: `->`, `->>`, `some->`, `some->>`, `as->`,
  `cond->`, `cond->>` -- real Clojure semantics, added as the first item
  off `ROADMAP.md`. All pure `core.logos` macros, no interpreter
  changes. `->`/`->>` need no hygiene at all (each step mentions the
  previous step's *form*, not a re-evaluation of it, so nothing is ever
  evaluated twice); `some->`/`some->>` use syntax-quote's `g#`
  auto-gensym for a single per-step hygienic temp (checking `x` itself
  for `nil` *before* threading it into the first form, not just each
  step's result afterward -- `(some-> nil (+ 2))` never attempts
  `(+ nil 2)`); `cond->`/`cond->>` call the `gensym` primitive directly
  instead, since their per-step temp needs to appear both inside one
  syntax-quote's own text (the `let` binding) and outside it (spliced
  into a conditionally-threaded step built as plain data beforehand) --
  a need `g#` auto-gensym alone can't satisfy, since it only guarantees
  consistency *within* one syntax-quote's literal text.
- **Basic numeric/utility functions**, `ROADMAP.md`'s second tier-1
  item: `not`, `identity`, `constantly`, `complement`, `inc`, `dec`,
  `zero?`/`pos?`/`neg?`/`even?`/`odd?`, `mod`, `min`/`max`, `comp`,
  `partial` -- all pure `core.logos`, plus four new Layer-1 primitives
  the pure-Logos layer needed and had no way to express itself:
  - `quot`/`rem`: integer-only, thin wrappers over Erlang's own
    `div`/`rem` (already exactly Clojure's `quot`/`rem` semantics --
    truncate toward zero, remainder's sign matches the dividend's).
    `mod` (floored, sign matches the *divisor*) is built on `rem` in
    pure Logos.
  - `apply`: `(apply f a b ... coll)` dispatches straight to
    `Logos.Eval.apply_fn/3` -- the same function every other
    callable-application path (ordinary calls, `Logos.Process`, host
    Elixir code) already goes through, so no interpreter/evaluator
    changes were needed, only this one primitive. `comp`/`partial` are
    both themselves built on it.
  - `str`: stringifies and concatenates any number of values,
    deliberately different from `Logos.Printer.print/1` for `nil`
    (`""`, not `"nil"`) and strings (passed through bare, not
    re-quoted) -- `print/1` answers "what reads back to this value,"
    `str` answers "what should a human see," matching Clojure's own
    `str` exactly for both cases. Everything else reuses `print/1`
    rather than reimplementing per-type stringification.

  `min`/`max`/`comp` call `logos.seq`'s `reduce`/`reverse` from inside
  `core.logos`, even though `logos.seq` hasn't loaded yet at the point
  `core.logos` itself is being read -- confirmed safe (and worth calling
  out as a new pattern for this file specifically) since a `fn` body only
  ever resolves its bare symbols against whatever's reachable *when it's
  actually called*, never at definition time; by the time anything calls
  `min`/`max`/`comp`, `logos.seq` has long since loaded.
- **Decimal literals** (`10.99M`, `3M`, `-2.5M`, matching Clojure's own
  `M`-suffixed `BigDecimal` syntax exactly): arbitrary-precision,
  exact-*scale* decimal arithmetic -- unlike `float` (IEEE 754, can't
  represent `0.1` exactly) and unlike `Logos.Ratio` (always reduces to
  lowest terms, so `10.10` loses its original "two decimal places" the
  moment it becomes a ratio), a decimal preserves the scale it was
  written with (`10.10M` prints back as `"10.10M"`, not `"10.1M"`).
  Backed by the `decimal` hex package (a new, genuine runtime
  dependency -- the first besides `ichor_runtime`; zero further
  dependencies of its own) -- `lib/logos/value/decimal.ex`
  (`Logos.Decimal`) is a thin wrapper module, not a new struct: Logos
  decimal values *are* the hex package's own `%Decimal{}` directly.
  `+`/`-`/`*`/`/`/`=`/`<`/`>`/`<=`/`>=`/`number?`/`type-of` all gained
  decimal-awareness, and `logos.core` gained a `decimal?` predicate
  (alongside the existing `nil?`/`list?`/`vector?`/`map?`/`set?`/
  `number?`). Required a one-line change to `priv/grammar/logos.aether`
  (`NUMBER`'s plain int/float alternative gained an optional trailing
  `"M"`) and regenerating the checked-in `lib/logos/reader/generated.ex`
  -- the grammar's first change since the project's initial commit.

  **Numeric tower / contagion rules**, closest to Clojure's own plus one
  deliberate, documented simplification: `Integer < Ratio < Decimal <
  Float`, each wider type winning when mixed with a narrower one.
  Decimal+Ratio converts the ratio to a decimal via `Decimal.div/2` at
  the ambient context precision (real Java `BigDecimal` *throws* on a
  non-terminating ratio like `1/3` instead; Logos rounds, since
  `Logos.Ratio`'s own design already isn't a literal Java-BigDecimal
  port). "Float poisons everything" (mixing a float into ratio/decimal
  math always produces a float, matching Clojure's own well-documented
  double/BigDecimal-mixing gotcha) applies to both Ratio and Decimal.
  `=` stays scale-sensitive (matches real `BigDecimal.equals/1` exactly
  -- `(= 1.10M 1.1M)` is `false`; only `<=`/`>=` treat them as equal,
  via `Decimal.compare/2`).
- **Standard library split into real namespaces under `priv/stdlib/`**:
  `core.logos` (`logos.core` -- unchanged `defmacro`/`let`/`if`/`when`/
  `unless`/`and`/`or`/`defn`, plus new `defn-`/`doc`/`ns`),
  `seq.logos` (`logos.seq` -- new `map`/`filter`/`reduce`/`take`/`drop`/
  `reverse`/`count`/`empty?`, list-only, built purely from `first`/
  `rest`/`cons`/`cond`), `concurrency.logos` (`logos.concurrency` --
  `receive`/`atom`/`deref`/`swap!`/`reset!`, moved as-is from the old
  single-file `stdlib.lisp`), `logos.map` (new `get`/`assoc`/
  `dissoc` Layer-1 primitives -- no `.logos` file, since map access
  needs real Elixir map operations Logos has no way to express itself),
  and `test.logos` (`logos.test` -- see its own entry below).
  `Logos.Stdlib.load!/1` loads `core.logos` first, then the rest, then
  refers `logos.seq`/`logos.map`/`logos.concurrency` into `logos.core`
  itself -- combined with a small `Logos.Eval.resolve_symbol_location/2`
  change (below), this is what keeps every one of these functions/macros
  reachable with no namespace prefix from any namespace, exactly as if
  everything still lived in one file. `logos.test` is deliberately
  excluded from this refer-into-core step -- see its own entry.
- **`logos.test`**: a small unit-testing library -- `deftest`, `assert`,
  `assert=`, `assert-throws`, `run-tests`. Loaded into every
  `Logos.Runtime` like the other stdlib files, but -- unlike
  `logos.seq`/`logos.map`/`logos.concurrency` -- deliberately *not*
  referred into `logos.core`; testing macros have no business being
  unqualified-visible in every embedding's production namespaces, so a
  caller opts in explicitly: `(require '[logos.test :refer [:all]])`
  (this project's own `:refer [:all]` spelling for "refer everything,"
  a vector, unlike real Clojure's bare `:refer :all`). Each `deftest` is
  isolated in its own `spawn-monitor`ed process when `run-tests` runs it
  -- real process isolation, not a simulated `try`/`catch` sandbox, since
  Logos's `try`/`catch` only ever catches an explicit `(throw ...)`, never
  an ordinary evaluation error (an unbound symbol, a wrong arity, ...) --
  without it, one genuinely broken test would abort the whole
  `run-tests` call rather than being reported as one failure among many.
  `run-tests` returns a summary map: `{:total n :passed n :failed (list
  of (name reason) pairs)}`.
- `normal-exit?` new Layer-1 primitive: whether a `:DOWN`/`:EXIT`
  message's `reason` was a clean exit. Needed because that reason is a
  raw Elixir term (the bare atom `:normal` on a clean exit) -- `:normal`
  in Logos source reads as a `Logos.Keyword`, a different, never-`==`
  type, so `(= reason :normal)` is always false even for a genuinely
  clean exit. `logos.test`'s `run-tests` is what surfaced the need for
  this (checking whether a spawned test process exited cleanly).
- `ns`, `doc`, `defn-` macros (`core.logos`) -- previously planned but
  never implemented. `ns` is sugar over `in-ns`+`require`/`use` (not
  real file-based namespace loading, which nothing in Logos has yet);
  `doc` reads back a var's docstring; `defn-` is `defn` plus `^:private`.
- `with-meta`/`var-doc`/`string?` new Layer-1 primitives, backing
  `defn-`/`doc`/`defn`+`defmacro`'s optional-docstring detection
  respectively.
- Keyword-as-function (`(:key m)`/`(:key m default)`, `Logos.Eval.apply_fn/3`),
  matching Clojure's own keyword-as-`IFn` behavior. Scoped to a map or
  `nil` argument (not sets).
- **Docstrings throughout the stdlib**, and `^:macro`/`^:private`
  metadata used directly wherever it fits, rather than only `:private`:
  `defmacro` itself now flags the macro it defines via `with-meta`+
  `{:macro true}` (one `def`) instead of a separate `set-macro!` call
  (its own bootstrap definition is flagged the same way, via `^:macro`
  reader sugar); `defn`/`defmacro`/`defn-` all accept an optional leading
  docstring argument (`(defn name "doc" [params] body...)`), matching
  Clojure. `Logos.Eval`'s `meta_from_symbol/2` now recognizes `:macro`
  alongside the existing `:private`, both read off a `def`'d symbol's own
  metadata (`^`-attached or `with-meta`-attached).
- `test/logos_scripts/` -- hand-written `.logos` test scripts exercising
  the language end to end from Logos source itself (not just from the
  Elixir side): short feature tests (arithmetic/ratios, collections/seq,
  macros/hygiene, namespaces, concurrency) plus two larger, more
  realistic examples (an atom-backed key-value store; a spawn-based
  worker pool computing a parallel sum of squares). Each script
  `(require '[logos.test :refer [:all]])`s and registers its scenarios
  with `deftest`; `test/logos_scripts_test.exs` runs each script and
  reads its final `(run-tests)` summary, flunking on any reported
  failure (a failing `deftest` no longer makes the whole script's
  evaluation return `{:error, _}`, since each is isolated in its own
  process -- see `logos.test`'s own entry above).

- **Reader/grammar** (`priv/grammar/logos.aether`, `Logos.Reader`,
  `Logos.Reader.Actions`): a Lisp reader built on Ichor/Aether, extending
  the base fixture grammar with set literals (`#{...}`), character
  literals (`\a`, `\newline`, `\uHHHH`), ratio literals (`1/3`),
  var-quote (`#'sym`), anonymous-fn sugar (`#(...)`), real symbol/
  collection metadata (`^meta`), and `#_form` datum comments. Pure
  reification only -- the reader never evaluates.
- **Eval / special forms** (`Logos.Eval`): a tree-walking evaluator
  implementing exactly six special forms (`quote`, `cond`, `do`, `def`,
  `fn`, `try`), written so a tail-position call is a genuine Elixir tail
  call -- validated with a real 10,000,000-iteration self-recursive test.
  `try`/`throw`/`catch`/`finally` interpose on a dedicated
  `Logos.Thrown` exception, keyword-tag matched.
- **Macroexpand / hygiene** (`Logos.Macroexpand`, syntax-quote desugaring
  in `Logos.Reader.Actions`): a separate macroexpansion pass to a fixed
  point, with lexical-shadow tracking (a local can shadow a same-named
  macro), implicit `&form`/`&env` bindings, real nested-depth
  syntax-quote (`` ` ``/`~`/`~@`), automatic symbol qualification, and
  automatic gensym (`x#`) for macro hygiene.
- **Namespaces, Vars, Runtime** (`Logos.Runtime`, `Logos.Namespace`,
  `Logos.Var`): a per-embedding-instance, `:public` ETS-backed namespace
  registry (never a global singleton) with interned Vars, `require`/
  `use`/aliasing, an implicit `logos.core` refer on every namespace,
  private-var support (`^:private`), circular-require detection, and
  per-process current-namespace tracking so concurrent `spawn`ed
  processes never stomp on each other's `in-ns`.
- **Bootstrap layering**: Layer 1 Elixir primitives (`Logos.Primitives`)
  plus a self-hosted Layer 2 (`lib/logos/stdlib.lisp`, loaded via
  `Logos.Stdlib`) written in Logos itself -- `defmacro` bootstrapped from
  two lines, then `if`/`let`/`when`/`unless`/`and`/`or`/`defn`/`receive`/
  `atom`/`deref`/`swap!`/`reset!` built on top.
  `Logos.new_runtime/1` is the one-call convenience combining both.
- **Concurrency** (`Logos.Process`, `Logos.Atom`, `Logos.Pid`): real BEAM
  process primitives (`spawn`/`spawn-link`/`spawn-monitor`/`link`/
  `monitor`/`send`/`self`/`exit`), hand-rolled selective receive
  (`receive-match!`) with a `receive` macro over it, and atoms as an
  ordinary Logos-defined stateful loop process (not an `Agent`).
- **Dev tooling**: `Logos.Printer` (a round-tripping textual printer for
  every reader-producible type), `Logos.Format` (a comment-preserving
  code formatter built on the raw token stream), `Logos.Repl`, and four
  Mix tasks -- `mix logos.repl`, `mix logos.run`, `mix logos.format`,
  `mix logos.remsh`.
- Full documentation pass: in-code `@moduledoc`/`@doc`/`@spec` coverage
  and private-function/complex-logic comments across every module and
  `stdlib.lisp`; an ExDoc configuration; this README, the library and
  language tutorials/examples/cheatsheets, and this changelog.
- **Real file-based namespace/`require` loading.** `require`/`use`/`ns`
  now resolve a namespace not already loaded in-memory against a new
  `Logos.Runtime.new/1` opt, `:load_paths` (a fixed, per-`Runtime` list
  of directory strings, exposed via `Logos.Runtime.load_paths/1`),
  following Clojure's own `ns`-to-classpath naming convention
  (`my-app.core` -> `my_app/core.logos`, first load path with a match
  wins). `mix logos.run`/`mix logos.repl` both configure `["lib"]`.
  Three new, narrow error shapes distinguish the failure modes only a
  real disk loader has: `{:cannot_read_ns_file, ns, path, reason}`, `
  {:ns_file_missing_ns, ns, path}` (the file evaluated but never `(ns
  ...)`'d the namespace it was required under), and the pre-existing
  `{:namespace_not_loaded, ns}` now also covers "no matching file on any
  load path". The requiring process's current namespace is saved and
  restored around a load, mirroring Clojure's `load` dynamically
  rebinding and popping `*ns*`. The circular-require guard
  (`Logos.Runtime.start_loading!/2`/`finish_loading!/2`, already built,
  previously unreachable) is exercised by real recursion for the first
  time -- `{:error, {:circular_require, ns}}` for a genuine A-requires-B-
  requires-A cycle.
- **Exponent number syntax** (`1.5e10`, `1e5`, `1.5e-10`, and
  `1.5e10M`/`1e5M` for decimals), matching Clojure's own grammar.
  `priv/grammar/logos.aether`'s `NUMBER` rule gained an optional
  exponent group (`("e" | "E") ("+" | "-")? DIGIT+`) on its plain
  int/float alternative, ahead of the optional `"M"` suffix; the sign is
  optional-and-either (not minus-only) since `Decimal.to_string/1`'s own
  default `:scientific` format prints a leading `+` for a positive
  exponent, so accepting it isn't just source ergonomics -- a decimal
  with a large-magnitude exponent couldn't otherwise round-trip through
  the printer at all.

### Changed

- **`(empty? x)`/`(count x)`/etc. on a non-collection argument now
  errors instead of silently returning `false`/a no-op** -- a side
  effect of `logos.seq` now coercing every `coll` argument via `to-list`
  (see this release's seq-abstraction entry above): `(empty? 5)` used to
  fall through to `false` (neither the `nil` nor `()` check matched),
  now it errors with `to-list`'s own `{:invalid_args, ...}`. Matches
  real Clojure, where `(empty? 5)` also throws -- more correct, not a
  regression, but a real, observable difference for any existing caller
  that relied on the old silent-`false` behavior.
- **Renamed the Logos source file extension from `.lisp` to `.logos`**
  throughout -- the stdlib's own files, `mix logos.repl`'s project-file
  preload glob, `mix logos.run`/`mix logos.format`'s usage text and doc
  examples, and every doc-comment cross-reference to the (now four)
  stdlib files.
- `Logos.Eval.resolve_symbol_location/2`'s auto-refer-core fallback now
  also checks `logos.core`'s own `refers` table (one level, not a
  recursive namespace walk) when a name isn't directly interned there --
  what makes the standard-library namespace split above possible without
  losing "usable everywhere with no namespace prefix."
- `Logos.Primitives.install!/1` installs primitives into more than one
  namespace now (`logos.core`'s existing set, plus the new `logos.map`),
  rather than always `logos.core`.

- **Upgraded to the latest Ichor** (GLR/LR parsing engines, custom
  lexemes/rules, a token refiner, a `Backtrack` unification engine, and a
  `Toolkit` of extracted compiler-internals helpers). Evaluated every new
  subsystem against Logos's actual needs rather than adopting for its own
  sake: `use Ichor`/`Ichor.Actions`'s public contract is unchanged, so no
  migration was required there. Two genuine, narrow wins were adopted:
  `Logos.Eval`'s private `eval_args/3` now uses
  `Ichor.Toolkit.Result.map_ok/3` instead of a hand-rolled
  accumulate-and-reverse recursion (same behavior, less bespoke code).
  GLR/LR, `Toolkit.Pratt`, `Toolkit.TypeScheme`, `Toolkit.Layout`,
  `Toolkit.Codegen`, `Toolkit.Graph`, and `Ichor.Backtrack` were all
  deliberately **not** adopted -- Logos's grammar is a non-left-recursive,
  non-layout-sensitive, prefix-only S-expression reader with no infix
  operators, no type inference, and no unification/logic-search feature,
  so none of those apply. `Toolkit.TermWalk`/`Toolkit.Fixpoint` were also
  evaluated and rejected for `Logos.Macroexpand`/the syntax-quote
  desugarer specifically: both need context threading (locals tracking,
  gensym-table state), `quote`-suppression, and per-call-site
  re-expansion that those generic primitives don't have a hook for --
  adopting them would have added code, not removed it.
- **Fixed a real, previously-documented gap using the Ichor upgrade as
  the occasion to revisit it**: `SYMBOL_CHAR` now includes `.`, so dotted
  namespace symbols (`my-app.core/x`, `ns.sub/x`) tokenize as ordinary
  symbols. Previously `.` fell through to no grammar rule at all, so a
  namespace could *hold* a dotted name (an ordinary Elixir string, e.g.
  `"logos.core"`) but the reader could never produce one from source
  text -- this was a plain one-line grammar fix, not something that
  needed any of Ichor's new machinery (`.` staying out of `SYMBOL_START`
  was a deliberate, separate choice: Logos has no `.method`/`.-field`
  interop sugar today, so a leading dot is still a hard read error
  rather than silently becoming a one-character symbol).

- **Ichor dependency switched from a local path dependency to Hex, then
  from `use Ichor` to `mix ichor.gen`, splitting the dependency into
  `ichor` (dev-only) + `ichor_runtime` (runtime)**, following the same
  split upstream in Ichor itself. Both are now ordinary Hex packages --
  `{:ichor, "~> 0.2.1", only: [:dev], runtime: false}` and
  `{:ichor_runtime, "~> 0.1.0"}`, the latter
  [independently published](https://hex.pm/packages/ichor_runtime) (its
  own repo, not a subdirectory of `ichor`'s). `lib/logos/reader/generated.ex`
  is `mix ichor.gen`'s checked-in output for `priv/grammar/logos.aether`
  -- `Logos.Reader` now delegates `run_sequence/2`/`tokenize/1` to it
  instead of having Ichor's native codegen splice
  `parse/1`/`tokenize/1`/`run/1,2`/`run_sequence/2` directly into
  `Logos.Reader` at every compile. Regenerate it (command in
  `mix.exs`'s `deps/0`) whenever `priv/grammar/logos.aether` changes.
  The practical win: `ichor` (the Aether front-end, `Grammar.Analysis`,
  the LR/GLR table builder, both codegen backends) is `only: [:dev],
  runtime: false` and never ships, including in a `mix release` build --
  only `ichor_runtime` (`Ichor.Actions`, `Ichor.Error`,
  `Ichor.Toolkit.Result`, the compiled tokenizer/parser combinators) is
  an ordinary runtime dependency, exactly the handful of support modules
  the generated reader actually calls.
- Removed every internal citation of `DESIGN.md` (a former internal design
  document, never published, now deleted) and of its numbered `Phase N`
  rollout plan from module docs and comments across the codebase. Every
  fact those citations pointed at is now stated directly, in place, so no
  doc comment depends on a document that doesn't ship with the library.

### Fixed

- **`&` (`fn`'s rest-param marker) inside a syntax-quoted macro
  template** (found while building `defmulti`, which needs to build
  exactly this shape -- `` `(defn ~name [& args#] ...) ``): the *same
  bug class* as the `catch`/`finally` fix below, a second instance of
  it. `Logos.Reader.Actions`'s `@special_form_names` (the list
  syntax-quote auto-qualification skips) had `catch`/`finally` but not
  `&`. A syntax-quoted `[& args#]` auto-qualified the bare `&` to e.g.
  `user/&` (not a real Var anywhere, so resolution falls back to
  qualifying against the current namespace) -- and a qualified symbol
  never satisfies `Logos.Eval`'s `rest_marker?/1` (bare, `ns: nil`
  names only), so the parameter silently stopped being recognized as a
  rest marker at all: the resulting function came out with a *fixed*
  arity of 2 (`&` and the following name both treated as ordinary
  positional params) instead of "any number of arguments." Confirmed
  end to end (`test/logos/syntax_quote_test.exs`) with a minimal macro
  reproducing exactly this shape, independent of multimethods
  themselves -- this was a real, general language bug, not something
  specific to `defmulti`.
- **A Logos map *literal* with computed elements silently didn't
  compute them** (found in `defmulti`/`defmethod`'s own first draft):
  `register-multimethod!` built `{:dispatch-fn dispatch-fn :methods
  {}}` as a literal map, which -- per `Logos.Form.t()`'s own documented
  behavior, map/vector/set literals are self-evaluating with
  *unevaluated* elements, unlike Clojure -- stored the literal symbol
  `dispatch-fn` as the value, not the function actually bound to that
  parameter. Every multimethod dispatch then failed with
  `{:not_callable, #Logos.Symbol<dispatch-fn>}`. Fixed by building the
  map via `assoc` from `{}` instead, the same fix shape `into`/
  `frequencies` (`priv/stdlib/seq.logos`) already needed for the
  identical reason -- not a new class of bug, but a real instance of an
  already-known trap, caught by an actual `mix run` smoke test before
  it reached a test file.
- `Logos.Repl` and `Mix.Tasks.Logos.Run` each had a private
  `format_error/1` helper that called `Exception.message/1` on a
  `%Ichor.Error{}` value -- but `Ichor.Error` is a plain struct, not an
  `Exception`, so this would have raised `Protocol.UndefinedError` the
  first time a reader-level error actually reached either code path (a
  REPL/`mix logos.run` syntax error in the source being read, as opposed
  to an evaluation error). Both now call `Ichor.Error.format/1`, the
  function Ichor itself uses internally to render this same struct.
- `Logos.Process`'s docs referenced `Process.spawn_link` and
  `Process.spawn_monitor` (both arity 1) -- neither exists in
  Elixir/Erlang (verified directly:
  `function_exported?(Process, :spawn_link, 1)` and the
  `:spawn_monitor` equivalent are both `false`). The real functions are
  `Kernel.spawn_link/1`/`Kernel.spawn_monitor/1`, which is what
  `Logos.Process.spawn_link/2`/`spawn_monitor/2` were already correctly
  calling -- only the doc text named the wrong module.
- Every guide's code examples and every module's technical claims were
  re-run against the real, compiled implementation rather than trusted
  as written, surfacing and correcting several other stale/incorrect
  claims: a contradiction in `LOGOS.md` about whether
  `(import 'String.upcase)` actually works (it does; one code block said
  otherwise), a stale claim that dotted `in-ns`/`require` namespace names
  are still a reader error (fixed earlier, doc never caught up), a stale
  `Logos.Var` "doesn't exist until a later phase" claim in the reader
  actions' `var-quote` doc, a stale "no concurrency primitives exist yet"
  caveat in `Logos.Runtime`'s moduledoc, and a stale claim in
  `Logos.Eval.resolve_symbol_location/2`'s doc that lexical-shadow
  tracking for macro calls "is not yet implemented" (it is).
- `mix format`/`mix logos.format --check-formatted` now both pass cleanly
  (previously-unformatted whitespace/indentation across most of `lib/`
  and `lib/logos/stdlib.lisp`, unrelated to this pass's actual doc
  content, brought in line with the project's own stated style).
- `mix docs` previously emitted 9 ExDoc cross-reference warnings (doc
  comments linking to private/hidden functions via
  `` `Module.function/arity` `` backtick syntax, which ExDoc's autolinker
  can't resolve). Rephrased each to convey the same information without
  triggering autolinking; `mix docs` now builds with zero warnings.
- **`#(...)` anon-fn sugar** (`Logos.Reader.Actions`'s `desugar_anon_fn/1`):
  previously spliced the captured body forms directly into the generated
  `fn`'s body instead of nesting them as one call -- `#(+ %1 %2)`
  desugared to `(fn [%1 %2] + %1 %2)` (three sequential body forms,
  returning only `%2`) instead of `(fn [%1 %2] (+ %1 %2))`. Also, a bare
  `%` placeholder synthesized the `%1` param but left the body
  referencing the never-bound literal symbol `%`. Both fixed: the body
  now nests as one call, and every bare `%` in it is substituted to `%1`.
- **`macro?`** now resolves through the same full chain
  (`Logos.Eval.resolve_symbol_location/2`) actual macro dispatch uses,
  instead of a current-namespace-only lookup -- `(macro? 'let)` now
  correctly returns `true` (it only ever returned `false` before, since
  `let` is reached via the implicit `logos.core` refer, never `def`'d
  directly in the caller's own namespace).
- **Ratio arithmetic and comparison** (`Logos.Primitives`): `/` now
  accepts an already-constructed `%Logos.Ratio{}` operand and
  cross-multiplies instead of erroring (`(/ (/ 1 3) 2)` => `1/6`); `+`/
  `*` (and `-`, which had the identical bug, undocumented until this
  pass) accept a ratio operand instead of raising `ArithmeticError`;
  `<`/`>`/`<=`/`>=` now cross-multiply to compare fraction magnitude
  instead of comparing `%Logos.Ratio{}` struct fields directly
  (`(< 1/3 1/2)` now correctly returns `true`, previously `false`).
- **Ratio mixed with a float crashed** (found while designing decimal's
  own numeric-tower contagion rules): `num_add/2`/`num_sub/2`/`num_mul/2`
  had clauses for ratio-vs-ratio and ratio-vs-integer, but none for
  ratio-vs-float -- a bare float operand fell through to the generic
  `a + b` fallback, and `%Logos.Ratio{} + 1.5` isn't valid Erlang
  arithmetic (`%Logos.Ratio{}` isn't a number). `(+ 1/3 1.5)` crashed;
  now produces a float (`1.8333333333333333`), matching the same "float
  poisons everything" rule decimal arithmetic uses.
- **`catch`/`finally` inside a syntax-quoted macro template** (found
  while building `logos.test`'s `assert-throws`, which is exactly this
  pattern): `Logos.Reader.Actions`'s `@special_form_names` (the list
  syntax-quote auto-qualification skips) had `try` but not its own
  `catch`/`finally` clause-introducers. A syntax-quoted `` `(try ~form
  (catch ~tag v# v#)) `` auto-qualified `catch` to e.g. `some-ns/catch`
  (not a real Var anywhere, so resolution fails and it falls back to
  qualifying against the macro's defining namespace) -- and a qualified
  symbol never satisfies the private clause-boundary check `Logos.Eval`'s
  `try` special form uses internally (bare, `ns: nil` names only), so the
  clause silently stopped being recognized as a catch at all and became
  dead body code instead: the
  exception it was meant to catch propagated uncaught. Every syntax-quoted
  `try`/`catch`/`finally` written before `logos.test` was either in
  `core.logos`'s own bootstrap (`catch`/`finally` never appear there) or
  written directly by hand (never through syntax-quote), which is why
  this went unnoticed until now.
- **`Logos.Printer`'s "very large/small floats don't round-trip" gap**:
  `Float.to_string/1` falls back to scientific notation (`"1.0e300"`)
  past a certain magnitude, and until this pass the grammar had no
  exponent syntax to read that text back with. Closed by the exponent
  syntax addition above -- confirmed with an actual round-trip test
  (`1.0e300`/`1.0e-300` through `print/1` then `Logos.Reader.read/1`),
  not just inferred from the grammar change.
- **The circular-require guard could never actually fire**, a bug found
  while wiring up real disk loading: `load_ns!/2`'s existing
  `Namespace.exists?/2` fast-path was checked *before* the `loading?`
  check, but a namespace file's own leading `(ns circ.a ...)` form makes
  `Namespace.exists?(runtime, "circ.a")` true the moment its `in-ns` line
  runs -- well before the rest of the file (and whatever it itself
  requires) finishes evaluating. So a genuine A-requires-B-requires-A
  cycle would silently "succeed" (both namespaces end up created, just
  incompletely) instead of tripping the guard, since by the time B
  re-requires A, A already "exists" as an empty, still-loading shell.
  Fixed by checking `loading?` first -- the same distinction real
  Clojure draws between "namespace object created" (`in-ns`/`create-ns`)
  and "fully loaded" (`*loaded-libs*`).

See [CONTRIBUTING.md](CONTRIBUTING.md) for the current (empty) known-gaps
list.
