API Reference Logos v#0.2.0
Copy MarkdownModules
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.