Logos. Reader. Actions
(Logos v0.2.0)
Copy Markdown
Ichor.Actions implementation for priv/grammar/logos.aether.
Architectural rule: this module does pure reification only -- it
turns a parsed Aether node into plain Logos.Form.t() data. It never
evaluates Logos code, never resolves a symbol, never runs a macro.
Logos.Macroexpand/Logos.Eval are entirely separate, later passes
over the plain data this module builds.
Design decisions this module makes
nil/true/falseare reified directly to the Elixir valuesnil/true/false(not%Logos.Symbol{name: "nil"}etc.) right here in theSYMBOLtoken handler --Logos.Form.t()includesnil/boolean()directly as base cases (seeLogos.Form's own moduledoc), which only makes sense if the reader already produces them; nothing later (macroexpand, eval) needs to special-case these three symbol spellings again.String escapes: the base fixture grammar's
STRINGtoken has no escapes at all (a string literal can't contain a literal", which is unusable for a real language) -- the grammar was extended to tokenize\-escaped characters, and this module decodes the practical subset Logos actually supports:\",\\,\n,\t,\r. Any other\xsequence is left as the two literal characters\andx-- a conservative choice (never silently drops a byte) documented here rather than raising, since a full escape table (\uHHHHin strings, octal, etc.) is intentionally out of scope for now.quasiquote_sugar/unquote_sugar/unquote_splice_sugar: real syntax-quote desugaring, see the "Syntax-quote desugaring" section below.handle_rulefor these three rules still just reifies (wraps the already-evaluated inner form in a private sentinel tuple, exactly the same pattern as@discard) -- the real recursive, depth-tracking, auto-qualifying, auto-gensym'ing transformation intolist/concat/quotecalls happens as a single separate pass (desugar_syntax_quote/2, exported forLogos.Readerto call once per top-level form after the whole form has been reified), not insidehandle_ruleitself. See that section for why:Ichor.Actions.eval_all's per-node dispatch is bottom-up, so ifhandle_rule(:quasiquote_sugar, ...)tried to fully desugar right there, a nested backtick's ownhandle_rulecall would already have run (and already fully desugared itself) before the outer one ever saw it -- destroying the depth information nested syntax-quote needs. Keeping these three rules as cheap, uniform, order-independent reification (sentinel tuples, not symbol-headed lists -- so they can never collide with a user-written list that happens to start with a symbol literally namedquasiquote/unquote/unquote-splice) and doing the real depth-aware walk afterward, once, over plain already-reified data, is what makes the depth counter tractable.var_quote_sugar(#'sym) reifies as a(var sym)placeholder list --Logos.Varexists as a runtime data structure (lib/logos/var.ex), but nothing inLogos.Evalresolves a(var sym)form to one yet, so there's still nothing real for the reader to hand back.#_formdatum comments: the grammar captures the discarded form structurally (so parsing/token-consumption stays correct regardless of what's inside it), and this module evaluates-and-discards it, returning a private sentinel tuple,{:__logos_discard__}, that can never collide with real reified data (every realForm.t()isnil/boolean/struct/number/binary/list/map/MapSet-- never a bare Elixir tuple). Every rule that assembles aform*sequence (list/vector/map/set/anon_fn_sugar) filters this sentinel out of its captured elements before building the real collection;Logos.Reader.read_all/1does the same at the top level.#(...)anonymous-fn sugar desugars to(fn [params...] body...)by walking the already-reified body for%1..%9, bare%(same as%1), and%&(rest) placeholder symbols and computing the highest arity actually used. Nested#(...)are not specially excluded from the placeholder scan (real Clojure forbids nesting#()-- this module doesn't enforce that restriction, a known limitation worth tightening later).meta_sugar(^meta target) attaches the reifiedmetaform ontotarget.metawhentargetis a%Logos.Symbol{}(symbols carry ametafield for exactly this). For any other target shape (a list, vector, map, ...) there's nowhere on the plain Elixir term to hang metadata, so it's represented instead as a(with-meta target meta)placeholder list, to be given real meaning once collections grow metadata support.tagged_literal(#tag value) is the one rule that genuinely needsLogos.Runtimestate (ctx) during the parse itself, not just at the laterdesugar_syntax_quote/2pass -- resolving a tag againstLogos.Runtime's:data_readersregistry has to happen at read time so'#inst "..."still yields the resolved value underquote. The registered reader is always a plain Elixir function reached throughLogos.Interop.Allowlist(never an arbitrary Logos closure), called directly viaKernel.apply/3-- this is still "pure reification calling a plain Elixir helper," the same category of thingChar.parse/1already is, not a new dependency on the evaluator.
Summary
Functions
Finds and desugars every {:logos_qq, _} syntax-quote sentinel
anywhere in form (however deeply nested in ordinary lists/vectors/
maps/sets), against runtime (nil skips auto-qualification -- see
moduledoc). Exported for Logos.Reader to call once per top-level
form; also used internally whenever a firing ~/~@ substitution's
argument needs the same treatment (see moduledoc).
The #_form discard sentinel -- exported so Logos.Reader can filter it out of read_all/1's top-level results too.
Functions
@spec desugar_syntax_quote(term(), Logos.Runtime.t() | nil) :: term()
Finds and desugars every {:logos_qq, _} syntax-quote sentinel
anywhere in form (however deeply nested in ordinary lists/vectors/
maps/sets), against runtime (nil skips auto-qualification -- see
moduledoc). Exported for Logos.Reader to call once per top-level
form; also used internally whenever a firing ~/~@ substitution's
argument needs the same treatment (see moduledoc).
The #_form discard sentinel -- exported so Logos.Reader can filter it out of read_all/1's top-level results too.