Skip to content

Repository files navigation

Logos

Logos is a Clojure-flavored Lisp, embeddable in Elixir/BEAM applications. Source text is parsed via an Aether grammar (priv/grammar/logos.aether), compiled ahead of time through Ichor's PEG engine (mix ichor.gen, checked in as lib/logos/reader/generated.ex) rather than at every compile -- Logos only depends on Ichor's small ichor_runtime package at runtime, not the grammar compiler itself. The result is macroexpanded and evaluated by a small tree-walking interpreter with genuine tail-call optimization -- a self-recursive Logos function loops forever without growing the Elixir call stack (validated by a real 10,000,000-iteration test).

In plain English: this defines a function fib that computes the nth Fibonacci number the ordinary recursive way -- if n is less than 2, the answer is n itself; otherwise it's the sum of the two preceding Fibonacci numbers, computed by calling fib again on n - 1 and n - 2. defn (define function), cond (a chain of test/result pairs, Lisp's if-elsif-elsif... equivalent), and ordinary function calls written prefix-first ((+ a b) instead of a + b) are the only pieces of syntax at work here -- everything a newcomer to Lisp needs to read this one example.

(defn fib [n]
  (cond
    (< n 2) n
    true (+ (fib (- n 1)) (fib (- n 2)))))

(fib 10)
;;=> 55

Why embed a Lisp in Elixir?

BEAM applications sometimes want to let data, not just code shipped at compile time, describe behavior -- a rules engine, a scripting layer for power users, a small DSL for configuration that's more expressive than YAML. Logos gives you a real, small, sandboxable language for exactly that: every Logos.Runtime is an isolated, per-instance namespace/Var registry (never a global singleton), import only reaches Elixir functions a host application explicitly allowlists, and concurrency primitives (spawn/send/receive, atoms) are genuine BEAM processes, not a simulated abstraction on top of them.

Components

  • Reader (Logos.Reader, Logos.Reader.Actions) -- turns source text into plain data (Logos.Form.t()): numbers, strings, symbols, keywords, lists, vectors, maps, sets, characters, ratios. Pure reification only -- the reader never evaluates anything. This is also where syntax-quote (`/~/~@) desugars into list/concat/quote calls, with real nested-depth tracking, auto-qualification, and auto-gensym (x#).
  • Macroexpand (Logos.Macroexpand) -- a separate pass over the reader's plain data, expanding macro calls to a fixed point before evaluation ever starts. Tracks lexical shadowing ((let [if ...] (if ...)) calls the local if, not the macro) and gives every macro the implicit &form/&env bindings real Clojure macros have.
  • Eval (Logos.Eval) -- the tree-walking evaluator. Six special forms are wired directly into it (quote, cond, do, def, fn, try); everything else (if, when, unless, and, or, let, defn, defmacro, receive, defmulti, sorted collections, transients, set algebra, tree-walking, string manipulation, ...) is an ordinary macro or function written in Logos itself, across priv/stdlib/*.logos (one namespace per file -- logos.core, logos.seq, logos.concurrency, logos.multimethod, logos.set, logos.walk, logos.string, logos.test, plus the primitives-only logos.map -- see Logos.Stdlib's moduledoc). logos.core refers logos.seq/ logos.map/logos.concurrency/logos.multimethod into itself, so all four are reachable unqualified from anywhere; logos.set/ logos.walk/logos.string/logos.test are deliberately excluded from that and only available once required explicitly, matching real Clojure's own clojure.set/clojure.walk/clojure.string/ clojure.test being separate, explicitly-required namespaces too -- testing macros (and these three) have no business being unqualified-visible in every embedding's production namespaces by default.
  • Runtime, Namespaces & Vars (Logos.Runtime, Logos.Namespace, Logos.Var) -- each Logos.Runtime is its own :public ETS-backed namespace registry (never a global singleton), so an embedding application can run several independent Logos "worlds" side by side.
  • Concurrency (Logos.Process) -- spawn/spawn-link/spawn-monitor/ link/monitor/send/receive are real BEAM processes and real message passing, not a simulation. Atoms (atom/deref/swap!/ reset!) are ordinary Logos functions built on top of these, the same way you'd hand-roll a stateful process in plain Elixir.
  • Dev tooling -- Logos.Printer (round-tripping printer), Logos.Format (a comment-preserving code formatter), Logos.Repl, and four Mix tasks: mix logos.repl, mix logos.run, mix logos.format, mix logos.remsh.

Installation

Logos is not yet published on Hex. Until then, depend on it directly from its source:

def deps do
  [
    {:logos, path: "path/to/logos"}
    # or: {:logos, github: "your-org/logos"}
  ]
end

Once published, the usual form will apply:

def deps do
  [
    {:logos, "~> 0.1.0"}
  ]
end

Quick start

In plain English: Logos.new_runtime/0 builds one fresh, self-contained Logos "world" (a runtime) with the standard library already loaded into it. Logos.eval_string/3 then reads one Logos expression out of a plain Elixir string and runs it against that runtime, returning {:ok, value, env} on success -- first (+ 1 2 3), an ordinary addition. The second pair of calls shows that a runtime remembers things: (defn greet [name] ...) defines a function once, and a later, completely separate eval_string/3 call against the same runtime can still call greet -- definitions aren't scoped to one call, only to the runtime they were made in. Logos.Printer.print/1 turns a Logos value back into the text you'd type to produce it, which is why a Logos list prints as (:hello :world) rather than an Elixir-shaped term.

# A fresh, isolated Runtime -- Layer 1 primitives + priv/stdlib/*.logos already loaded.
runtime = Logos.new_runtime()

{:ok, value, _env} = Logos.eval_string("(+ 1 2 3)", runtime)
value
#=> 6

# Define something, then use it in a later call against the same runtime --
# `def` visibility persists on `runtime`, independent of the lexical `env`.
{:ok, _, _} = Logos.eval_string("(defn greet [name] (list :hello name))", runtime)
{:ok, value, _} = Logos.eval_string("(greet :world)", runtime)

Logos.Printer.print(value)
#=> "(:hello :world)"

For a script that's many top-level forms at once, use Logos.eval_string_sequence/3 instead -- it threads the runtime and lexical continuity across every form and returns the last value.

Where to go next

Development

mix deps.get
mix precommit

mix precommit runs the whole gate in one shot -- format, compile --warnings-as-errors, credo --strict, sobelow, test, dialyzer -- the same checks CI (if any) would run, in fast-to-slow order.

See CONTRIBUTING.md for how to propose changes, and CHANGELOG.md for release history.

License

MIT -- see LICENSE.

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages