API Reference Logos v#0.2.0

Copy Markdown

Modules

Top-level end-to-end API: text --[Reader]--> Logos.Form.t() --[Macroexpand]--> Logos.Form.t() --[Eval]--> value.

A Logos atom (mutable-reference-via-process): %Logos.Atom{pid: pid()}. This is unrelated to Elixir's :atom type (an interned symbolic constant) -- a Logos atom is a mutable reference cell, analogous to Clojure's atom.

A Logos character literal: \a, \newline, \space, \tab, \u0041.

The two default tagged-literal readers (#inst, #uuid), pre- registered into every fresh Logos.Runtime -- see that module's new/1 and lib/logos/reader/actions.ex's handle_rule(:tagged_literal, ...), which is what actually calls these at read time.

A Logos decimal literal: 10.99M, 3M, -2.5M -- Clojure-style arbitrary-precision, exact-scale decimal arithmetic. Unlike float (IEEE 754 binary floating point, 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".

Lexical scope, and only lexical scope. This is a plain immutable chained-frame environment: a frame is a bindings map plus a parent frame (or nil at the root). let-style lexical binding and fn closures are the only things this module is responsible for -- top-level def visibility is an entirely separate concern, handled by Logos.Namespace/Logos.Var against a Logos.Runtime (see Logos.Eval's moduledoc for how symbol resolution combines the two).

The tree-walking evaluator: eval(form, env, runtime). Implements exactly six special forms -- quote, cond, do, def, fn, try -- every other list-headed form is a function-position call (evaluate head, evaluate args, apply).

The Elixir exception carrying a primitive-level evaluation failure -- division by zero, an unbound symbol, a wrong-arity call, and every other case Logos.Eval/Logos.Primitives used to signal via a plain {:error, reason} return before this module existed. reason is exactly what that tuple's second element always was -- this is a mechanical change in how a failure propagates (raise instead of return), not in what failures look like.

A Logos closure: %Logos.Fn{params, variadic?, body, env, clauses}. env is the lexically-captured defining Logos.Env -- capturing it is the whole point of fn: it lets the closure see the bindings visible at its definition site even after that scope has otherwise returned, which is what makes it a closure rather than a plain function pointer.

The reader/macro-level data type: what Logos.Reader.read/1 and Logos.Macroexpand produce and consume.

The Logos code formatter (mix logos.format). Style rules: 2-space indent; one space between siblings; no space before a closing paren; defn/fn/let/ns/cond/try bodies indent 2 spaces from the opening form (not aligned to the first argument). This is deliberately the simplest consistent rule, not a full cljfmt-parity style guide.

The host-curated allowlist import is restricted to: Logos's import only ever pulls from this module's @entries map, never opens up arbitrary Module.function access to the underlying Elixir/Erlang runtime. This is what keeps import a sandboxed capability rather than a full escape hatch out of the Logos data model.

A Logos keyword: :a, :my-ns/x. Self-evaluating -- unlike a symbol, a keyword never resolves against an environment.

Interns %Logos.Keyword{} structs so that reading the same keyword text twice (:a ... :a) always yields the exact same struct value.

Raised when applying a macro (during Logos.Macroexpand.expand/3) fails -- e.g. the macro function itself errors, or is called with a mismatched arity. Macroexpansion has no {:error, _}-tuple convention of its own (see Logos.Macroexpand's moduledoc), so failures here are surfaced as a real exception, the same way a reader syntax error would be.

The macroexpansion pass: Logos data --[Logos.Macroexpand]--> Logos data, entirely separate from -- and running entirely before -- Logos.Eval.

Functions operating against a Logos.Runtime's ETS table -- there is no free-floating %Logos.Namespace{} struct someone could get out of sync with the registry; every operation here reads/writes runtime's table directly. See Logos.Runtime's moduledoc for the exact row shapes.

A Logos process handle: %Logos.Pid{pid: pid()}. Print-only: a pid can be displayed but never appears in source text, since there is no reader syntax that produces one.

Layer 1: the Elixir-implemented primitive functions every Logos program is ultimately built on -- arithmetic, comparison, collections, namespace/Var management, macros, concurrency, and dev-tooling introspection. (String operations are not primitives here; they reach Logos through importing allowlisted Elixir functions -- see Logos.Interop.Allowlist.) Every primitive takes (args, runtime) uniformly, where args is the already-evaluated argument list and runtime is the calling Logos.Runtime.

Logos.Printer.print/1 -- the textual printer. Every reader-producible type (numbers, strings, symbols, keywords, collections) must round-trip: print/1 followed by Logos.Reader.read/1 must yield an ==-equal value back. Runtime-only types (Pid, Atom, Fn) are exempt from that requirement -- they print informatively but have no reader syntax to read back into.

The BEAM-native concurrency primitive family: spawn, spawn-link, spawn-monitor, link, unlink, monitor, demonitor, trap-exits!, exit, self, send, receive-match!. Every function here is plain Elixir, called from Logos.Primitives' (args, runtime) dispatch against already-evaluated argument values -- none of them need macro-hood. (The one related piece that does need macro-hood is the Lisp-level receive macro in concurrency.logos, since its clauses must stay unevaluated until a match is found.)

The Elixir exception a Logos-level process (a Logos.Fn thunk running under spawn/spawn-link/spawn-monitor) raises when its body's evaluation raises Logos.EvalError (Logos.Eval's own primitive-level failure signal -- see that module's moduledoc). Deliberately re-raised as a real BEAM crash (rather than silently swallowed) so that link/monitor on the spawning side observe a genuine :EXIT/:DOWN signal carrying reason, exactly like any other process crash: a Logos function erroring is treated as "this process failed," the same as an Elixir process raising.

A Logos ratio literal: 1/3, -22/7.

The public reader: turns Logos source text into plain Logos.Form.t() data, via priv/grammar/logos.aether (an Aether grammar) and Logos.Reader.Actions (pure reification, see that module's docs).

Ichor.Actions implementation for priv/grammar/logos.aether.

A Logos record: %Logos.Record{type: kw, fields: %{kw => value}} -- built by priv/stdlib/core.logos's defrecord macro via the record primitive (Logos.Primitives), no other construction path. type is always a namespace-qualified Logos.Keyword (e.g. :user/point), fixed at defrecord-expansion time to the namespace defrecord was invoked from -- so two different namespaces' same-named record types never collide, the direct analogue of Clojure records being tied to their defining namespace at compile time. fields is keyed by Logos.Keyword, matching how idiomatic Logos maps already use keyword keys, so record field access ((:x point), (get point :x)) reads exactly like map field access -- see Logos.Eval.apply_fn/3's keyword-as-function clauses and Logos.Primitives' get/assoc.

The local REPL (mix logos.repl): a fresh Runtime with logos.core and project files preloaded, a stdin read-eval-print loop, and a Clojure-style prompt (user=>). History vars *1/*2/*3/*e are ordinary def'd Vars. Each entered form is evaluated in its own spawned+monitored process, which gives real crash isolation and (in principle) a way to cancel just that form -- see the Ctrl+C section below for exactly what's implemented today.

Logos.Runtime -- the per-embedding-instance, :public ETS-backed namespace registry. Never a global singleton: every embedder of Logos calls Logos.Runtime.new/1 (or the Logos.new_runtime/0 convenience) to get its own isolated table, so multiple independent Logos instances can coexist in the same VM without seeing each other's namespaces or vars. An earlier version of this codebase instead kept a single bare, module-level, :named_table ETS table shared by the whole VM; this module replaced that global-singleton design.

A named, VM-wide lookup table from an arbitrary String.t() name to a %Logos.Runtime{}. Its main consumer is mix logos.remsh: a host app that starts distributed (--sname/--name + cookie) registers its Runtime(s) here under a name, and a logos.remsh session attached to that node looks up "the Runtime the host app wants exposed" by name rather than needing a direct reference passed around from process to process.

A Logos sorted map: (sorted-map ...) / (sorted-map-by cmp ...).

A Logos sorted set: (sorted-set ...) / (sorted-set-by cmp ...).

Loads the Layer-2 standard library -- macros and functions written in Logos itself, layered on top of the Elixir-implemented Layer-1 primitives -- into a fresh Logos.Runtime. Source lives as real files under priv/stdlib/ (not embedded Elixir string constants), one per namespace

Builds the Markdown source for guides/language/stdlib/*.md -- one generated, per-symbol lookup document per file-backed stdlib namespace (Logos.Stdlib.namespaces/0), one for the six special forms Logos.Eval wires in directly, one for every Layer-1 primitive (Logos.Primitives.docs/0, see primitives_document/0), and a table-of-contents OVERVIEW.md linking to every one of them (see overview_document/0) -- ALL of these, OVERVIEW.md included, are generated and written by mix logos.gen_docs; none is hand-edited (OVERVIEW.md's own body is a static template, unlike the others' :doc-metadata-driven content, but it still goes through the exact same generate-and-write path, so hand-editing it would just get silently clobbered on the next regeneration like any other file here). Each namespace/primitives document covers every public Var, pulled live from its own :doc metadata (Logos.Primitives.install!/1 gives every primitive a real one too, sourced from the same Logos.Primitives.docs/0 data -- no more generic "Layer-1 primitive" placeholder), together with a runnable example: @examples/@special_form_examples/ @primitive_examples hold one Logos source snippet per documented item, actually evaluated (fresh Logos.new_runtime/0 inside its own isolated process, starting from namespace "user" -- see evaluate_example/2's own comment) at generation time and rendered alongside its real, freshly-computed printed result -- never a hand-typed "expected output" that could quietly drift from the code. Every stdlib-namespace entry (special forms/primitives are Elixir- implemented, so this doesn't apply to those two pages) also gets its own defining form's exact source, reconstructed from Logos.Reader.tokenize/1's position-preserving token stream rather than re-parsed by hand -- see extract_source_snippets/1's own comment.

A Logos symbol: x, my-fn, ns/x, +, /.

The Elixir exception carrying a Logos-level (throw :tag value). Logos.Eval's try special form catches this specifically (via a real Elixir rescue), matches tag against each catch clause's keyword with = -- Logos catches by keyword tag alone, with no exception-class hierarchy the way Elixir's own rescue has -- and binds value to the catch clause's bound name.

A Logos transient: (transient coll). Wraps a single-row, :private ETS table ({:value, current}), not a spawned process like Logos.Atom -- a deliberate, different choice from atoms, chosen because a transient's whole purpose is the opposite of an atom's: atoms are meant to be a shared, thread-safe reference cell (hence a process + message-passing, so BEAM's one-message-at-a-time guarantee gives atomicity for free); a transient is explicitly not meant to be shared -- real Clojure transients are single-thread-use only, and using one from a different thread (or after persistent!) is a documented error. A :private ETS table gets both of those for free, as genuine BEAM-enforced guarantees rather than a documented-only discipline: only the owning process may :ets.lookup/:ets.insert into it (any other process gets ArgumentError), and persistent!/1 deletes the table outright, so any further op against it raises the exact same ArgumentError -- one mechanism naturally covers both of Clojure's transient safety rules, not two separate checks. This also means a transient op is genuine O(1) in-place mutation, no message round-trip -- actually delivering the performance transients exist for in real Clojure, unlike a process-based design would have.

A Var's identity: %{ns, name}. Conceptually a Var is %{ns, name, value, meta} -- mutable, interned in a Logos.Namespace -- but only the ns/name pair lives on this struct; see below for where value/meta actually live.

A Logos persistent vector: [1 2 3].

Mix Tasks

Formats Logos source files, or checks that they're already formatted. Built on Logos.Format, which formats from Ichor's raw token stream (position- and text-preserving, so comments survive) plus a simple paren-nesting-depth indentation rule -- --check-formatted mirrors mix format --check-formatted.

Regenerates guides/language/stdlib/*.md -- an overview page, one special-forms page, and one page per stdlib namespace, all built from Logos.StdlibDocs.documents/0 (every entry paired with a real, freshly-evaluated example). Those files are checked in and generated; never hand-edit them (see each file's own header comment and Logos.StdlibDocs's moduledoc).

Attaches an interactive REPL to a running, distributed Logos node, mirroring iex --remsh. The target host application must already be running as a distributed node (started with --sname/--name and a shared cookie) and must have registered its Logos.Runtime(s) by name via Logos.Runtime.Registry. Each form entered here is shipped via :rpc.call to the target node and evaluated there -- this session never reads the target's ETS tables directly.

Starts a local Logos REPL: a fresh Runtime with logos.core and any project .logos files preloaded, then a stdin read-eval-print loop with a Clojure-style prompt (user=>).

Runs a Logos script file or an inline expression: builds a fresh Runtime, evaluates the source as a top-level form sequence, and exits with a status reflecting success or failure.