Contributing to Logos
Copy MarkdownDevelopment setup
Requires Elixir ~> 1.19 (see mix.exs) and a matching Erlang/OTP.
git clone <this repo>
cd logos
mix deps.get
mix test
Logos's reader is generated ahead of time by
Ichor's mix ichor.gen task, from
priv/grammar/logos.aether, into the checked-in
lib/logos/reader/generated.ex (see Logos.Reader's moduledoc). Both
Ichor dependencies are ordinary Hex packages:
{:ichor, "~> 0.3", only: [:dev], runtime: false}-- the grammar compiler itself, needed only to runmix ichor.gen, never at runtime.{:ichor_runtime, "~> 0.2"}-- the small support library the generated reader actually calls, independently published and maintained (its own repo, own release cadence, not a subdirectory ofichor's).
Both constraints are major.minor only, no patch pin (this project's own
convention -- see AGENTS.md's "Dependency versions" section), so a
future patch/minor release of either is picked up by an ordinary mix deps.update ichor ichor_runtime with no mix.exs edit needed, as long
as it stays within the same major version.
mix deps.get fetches both. If you change priv/grammar/logos.aether,
regenerate the reader:
MIX_ENV=dev mix ichor.gen priv/grammar/logos.aether \
--module Logos.Reader.Generated \
--actions Logos.Reader.Actions \
--out lib/logos/reader/generated.ex
If you're working on Ichor and Logos together, a local mix.exs override
on either dep (path: "../ichor" / path: "../ichor_runtime") works the
same way it would for any other dependency; just don't commit that
override.
The other runtime dependency, {:decimal, "~> 2.0"}, backs decimal
literals (10.99M, see Logos.Decimal) -- an ordinary Hex package,
zero further dependencies of its own, no special setup.
guides/language/stdlib/*.md are also generated -- an overview page, a
special-forms page, a primitives page, and one page per stdlib
namespace, each built from that item's own docstring plus a runnable,
freshly-evaluated example (Logos.StdlibDocs, see its moduledoc; new
stdlib examples live in its @examples/@special_form_examples module
attributes, new primitive examples in its @primitive_examples). Every
stdlib-namespace entry also shows its own defining form's exact source,
reconstructed automatically from priv/stdlib/*.logos via
Logos.Reader.tokenize/1 (extract_source_snippets/1) -- nothing to
maintain by hand there, it always reflects whatever the .logos source
actually says. Primitive docstrings themselves live in
Logos.Primitives's own @doc_data (see its moduledoc) -- that's also
what backs (doc +) etc. at the Logos level now, not just the generated
page. If you add or edit a stdlib defn/defmacro/import
docstring/example, or a primitive's own doc/example, regenerate them:
MIX_ENV=dev mix logos.gen_docs
mix test fails (test/logos/stdlib_docs_test.exs) if any checked-in
page has drifted from a fresh regeneration, or if any documented item is
missing an example (or has one that errors when evaluated).
Architecture, in one paragraph
Logos is three fully separate passes over increasingly-evaluated data:
the reader (priv/grammar/logos.aether, an Aether grammar, plus
Logos.Reader.Actions for pure reification -- text to plain
Logos.Form.t() data, no evaluation at all) feeds macroexpand
(Logos.Macroexpand, a fixed-point pass over that same plain data,
running entirely before evaluation starts) which feeds eval
(Logos.Eval, a tree-walking evaluator implementing exactly six special
forms -- quote/cond/do/def/fn/try -- with everything else,
including if/let/defmacro/receive/defmulti, an ordinary macro
or function defined across the eight priv/stdlib/*.logos files
(Logos.Stdlib's moduledoc has the exact namespace-per-file breakdown).
Logos.Runtime/Logos.Namespace/Logos.Var
sit underneath all three: a per-embedding-instance, ETS-backed namespace
registry, never a global singleton. If you're orienting yourself in the
codebase for the first time, read the moduledocs in roughly that order --
Logos.Reader -> Logos.Reader.Actions -> Logos.Macroexpand ->
Logos.Eval -> Logos.Runtime/Logos.Namespace/Logos.Var ->
Logos.Primitives/Logos.Stdlib -> Logos.Process (concurrency) ->
Logos.Printer/Logos.Format/Logos.Repl/Logos.StdlibDocs (dev
tooling) -- each
module's own moduledoc explains its role and how it fits with its
neighbors in detail; there is no separate architecture document to keep
in sync -- an earlier internal design document existed during initial
development and was deleted once the in-code documentation covered
everything it did, so the moduledocs themselves are now the single
source of truth.
Running things
mix precommit # the whole gate below, in one command
mix precommit (see its alias in mix.exs) runs, in fast-to-slow order:
mix format # Elixir source formatting (writes)
mix compile --warnings-as-errors # the compile-cleanliness bar this project holds itself to
mix credo --strict # static analysis
mix sobelow # security static analysis
mix test # full suite (500+ tests, plus property-based checks)
mix dialyzer # success-typing analysis (slowest step, especially the first PLT build)
Plus, separately (not part of mix precommit, since it's about .logos
sources, not .ex):
mix logos.format --check-formatted "priv/stdlib/*.logos" # Logos (.logos) source formatting
MIX_ENV=dev mix logos.gen_docs # regenerates guides/language/stdlib/*.md
The TCO test is slow by design. test/logos/tco_test.exs runs a real
10,000,000-iteration self-recursive Logos function to actually prove tail
calls don't grow the Elixir stack -- expect it to take roughly 70-90
seconds on ordinary hardware. This is the whole point of the test (a
smaller iteration count wouldn't distinguish "genuinely tail-call
optimized" from "just has a big-enough stack limit that a moderate
recursion depth doesn't blow it"); don't reduce its iteration count to
make the suite faster.
Code style
Elixir source:
mix format(see.formatter.exs); CI-equivalent check ismix format --check-formatted.Logos (
.logos) source:mix logos.format-- 2-space indent, one space between siblings, no space before a closing paren, body forms (defn/fn/let/cond/try/...) indented 2 spaces from the opening form rather than aligned to the first argument. Deliberately the simplest consistent rule, not fullcljfmtparity -- seeLogos.Format's moduledoc for exactly what it does and doesn't do (blank-line-count collapsing, no automatic reflow of long forms).Every public module needs a
@moduledocexplaining its role in the architecture above; every public function needs@doc(and a@specwhere one adds real information beyond what the function head already says); nontrivial private functions get a short#comment above them; anything genuinely subtle (a workaround, a real bug that got fixed a particular way, a design decision with a rejected alternative) gets a comment explaining why, not just what -- future contributors shouldn't have to rediscover a bug that already got fixed once. Look at any existing module (Logos.Eval,Logos.Macroexpand,priv/stdlib/core.logos) for the level of detail this project holds itself to.Logos-level (
.logos) functions/macros need a docstring too, exactly like an Elixir@doc-- eitherdef's own optional 3-arg form ((def name "doc" value)) ordefn/defmacro/defn-'s equivalent optional-docstring argument ((defn name "doc" [params] body...)), readable back via(doc name). Attach^:private/^:macrometadata (reader sugar on a literal symbol, or thewith-metaprimitive for a symbol built as macro-generated data) rather than a separateset-macro!call where it's adef-time property of the var being created -- seecore.logos's header comment for the one real constraint this has (never mark a helper inlogos.seq/logos.map/logos.concurrency/logos.multimethod/logos.set/logos.walk/logos.string/logos.test^:privateif a public function/macro in the same file calls it; it would silently break that public function/macro outside its own defining namespace -- Logos has no notion of "a closure's defining namespace" for bare-symbol resolution. A real instance of exactly this bug shipped and was caught by a smoke test during the missing-map/seq-functionality work -- seeCHANGELOG.md).A sharper version of the same hazard, for a namespace NOT auto-referred into
logos.core(logos.set/logos.walk/logos.string/logos.test-- seeLogos.Stdlib's moduledoc for which four those are and why): being public isn't enough there either. A function in one of these four calling ANOTHER function in the SAME file via a bare reference breaks whenever the caller only did(require '[logos.set :as set])(no:refer) -- the caller's own scope never pulled the callee in, only theset/fooqualified spelling, and the callee's body runs under the CALLER's namespace, not its own defining one. Fix: call it via the fully-qualifiedlogos.set/foo(orlogos.walk/foo/logos.string/foo) instead of a bare reference -- seelogos.set's own header comment for the full explanation and the actual smoke-test failure this was caught by.
Known gaps (honest list)
Gaps formerly listed here -- #(...) sugar splicing instead of nesting
its body, missing ns/doc/defn-, no seq-processing functions, no
map get/assoc/dissoc/keyword-as-function, ratio arithmetic/
comparison bugs, macro?'s refer-chain blind spot, no into, no
general type predicates, no real file-based namespace/require loading,
no exponent number syntax -- are fixed; see CHANGELOG.md. No known
functional gaps remain as of this pass; if you find one, add it here
along with the fix, or open an issue.
The one-time ROADMAP.md (a separate, forward-looking "Clojure features
Logos has never claimed to support yet" list -- threading macros,
destructuring, protocols/multimethods, sorted collections/transients,
and more) has been retired: every item it ever tracked shipped, and
nothing replaced it as a going-forward list -- this section is now the
single place to note any gap, bug or missing feature alike. If a
future gap is broad enough to want its own tracked, prioritized list
again, re-create a roadmap document rather than trying to force it back
into this section's own narrower "bugs in claimed behavior" shape.
If you fix a gap you add here later, please also update the corresponding note in guides/language/LOGOS.md and guides/language/LOGOS_CHEATSHEET.md.
Submitting changes
- Keep commits focused; explain why in the commit message, not just what.
- Add or update tests for any behavior change.
- Run the full verification set above before opening a PR:
mix precommit, plusmix logos.format --check-formattedif you touched any.logossource. - If you touch a
priv/stdlib/*.logosfile, remember it's dogfooded at startup (Logos.Stdlib.load!/1actually reads/macroexpands/evaluates every one of them) -- a broken macro there fails loudly the next time any test builds aLogos.RuntimeviaLogos.new_runtime/1, which is by design.test/logos_scripts/has hand-written.logostest scripts exercising the language end to end (run viatest/logos_scripts_test.exs) -- add to those too for a language-level behavior change, not just the Elixir-side ExUnit tests.