Logos. Primitives
(Logos v0.2.0)
Copy Markdown
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.
Each primitive is interned as an ordinary Var in the logos.core
namespace by install!/1, with its value set to a {:primitive, name} marker rather than a raw Elixir function. This means primitive
lookup goes through exactly the same symbol-resolution path as any
other Var (Logos.Eval.resolve_symbol_location/2): a bare reference to
+ in the user namespace resolves via user's implicit logos.core
refer, finds the Var, sees its value is a {:primitive, name} marker,
and Logos.Eval.apply_fn/3 dispatches accordingly to apply/3 below.
Besides the familiar arithmetic/comparison/list core (+ - * / = < > <= >= first rest cons list throw) and namespace machinery
(intern-var!/in-ns/require/use/import, set-macro!/macro?,
gensym, push-thread-binding!/pop-thread-binding! -- backing
core.logos's binding macro, see those primitives' own comments),
two small "collection primitives" -- vector and to-list
-- exist to make core.logos's let constructible from list/cons
alone (see that file's comments); concat/list->vector/list->map/
list->set support real syntax-quote desugaring
(lib/logos/reader/actions.ex), which needs a genuine n-ary concat
and a way to turn the resulting flat list back into a vector/map/set
for syntax-quoted [...]/{...}/#{...} literals.
Error convention
A primitive that can't do what was asked (division by zero, wrong
arity, an unbound symbol upstream, ...) raises Logos.EvalError
(Logos.Eval) rather than returning an {:error, reason} tuple --
reason is exactly what that tuple's second element always was. This
lets Logos-level try/catch intercept a primitive-level failure the
same way it already catches an explicit (throw ...) -- see
Logos.Eval's eval_try/3. Logos.eval_string/3's outermost boundary
rescues it back into {:error, reason}, so the public top-level API's
contract is unaffected.
Sorted collections and transients
compare, sorted-map/sorted-map-by, sorted-set/sorted-set-by/
sorted-set-put/sorted-set-remove, and transient/conj!/assoc!/
dissoc!/disj!/pop!/persistent! are Elixir-level of necessity,
not by choice: Logos.SortedMap/Logos.SortedSet (genuine new value
types, mirroring Logos.Vector) need real access to their own sorted
association-list representation, and Logos.Transient needs real
:ets mutation -- see those three modules' own moduledocs for the full
reasoning (why a sorted association list rather than a balanced tree,
why a :private ETS table rather than a process+message design like
atom). logos_compare/2 (this module's private ordering logic,
shared by compare and every sorted collection's default comparator)
and comparator_fn/2 (wraps a sorted-map-by/sorted-set-by caller-
supplied Logos function into the plain 2-arity Elixir function those
two value types need, via Logos.Eval.apply_fn/3 -- the same call-back
mechanism apply itself already uses, no new evaluator machinery) sit
at the very end of this file, alongside the pre-existing numeric-tower
comparison helpers they build on.
Concurrency primitives
spawn/spawn-link/spawn-monitor/link/unlink/monitor/
demonitor/trap-exits!/exit/self/send/receive-match! dispatch
to Logos.Process -- see that module's moduledoc for the real
BEAM-native mechanics (genuine processes, real receive/after, real
links/monitors). pid->atom wraps the %Logos.Pid{} a spawn call
returns into a %Logos.Atom{}: concurrency.logos's atom function is an
ordinary Lisp function (not built on Elixir's Agent) that spawns a
stateful loop process, and since Logos has no generic "construct an
arbitrary struct" facility from Lisp code itself, this one-line
identity wrapper is the minimal Elixir-level plumbing that requires --
the atom's actual stateful behaviour lives entirely in concurrency.logos's
atom-loop.
Summary
Functions
Applies the primitive named name to already-evaluated args, against runtime. Called by Logos.Eval.apply_fn/3 whenever the callable value is a {:primitive, name} marker. Raises Logos.EvalError on failure -- see moduledoc.
Every primitive's own {shape, doc} pair, keyed by name -- see @doc_data's own comment.
Interns every Layer-1 primitive as a {:primitive, name}-valued Var,
grouped by namespace (logos.core's existing set, plus the new
logos.map), with real :doc metadata from @doc_data/docs/0.
Logos.Stdlib.load!/1 refers logos.map into logos.core after this
runs, the same way it refers the .logos-file namespaces -- see that
module's moduledoc.
Functions
@spec apply(String.t(), [term()], Logos.Runtime.t()) :: term()
Applies the primitive named name to already-evaluated args, against runtime. Called by Logos.Eval.apply_fn/3 whenever the callable value is a {:primitive, name} marker. Raises Logos.EvalError on failure -- see moduledoc.
Every primitive's own {shape, doc} pair, keyed by name -- see @doc_data's own comment.
@spec install!(Logos.Runtime.t()) :: :ok
Interns every Layer-1 primitive as a {:primitive, name}-valued Var,
grouped by namespace (logos.core's existing set, plus the new
logos.map), with real :doc metadata from @doc_data/docs/0.
Logos.Stdlib.load!/1 refers logos.map into logos.core after this
runs, the same way it refers the .logos-file namespaces -- see that
module's moduledoc.