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/false are reified directly to the Elixir values nil/true/false (not %Logos.Symbol{name: "nil"} etc.) right here in the SYMBOL token handler -- Logos.Form.t() includes nil/boolean() directly as base cases (see Logos.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 STRING token 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 \x sequence is left as the two literal characters \ and x -- a conservative choice (never silently drops a byte) documented here rather than raising, since a full escape table (\uHHHH in 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_rule for 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 into list/concat/quote calls happens as a single separate pass (desugar_syntax_quote/2, exported for Logos.Reader to call once per top-level form after the whole form has been reified), not inside handle_rule itself. See that section for why: Ichor.Actions.eval_all's per-node dispatch is bottom-up, so if handle_rule(:quasiquote_sugar, ...) tried to fully desugar right there, a nested backtick's own handle_rule call 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 named quasiquote/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.Var exists as a runtime data structure (lib/logos/var.ex), but nothing in Logos.Eval resolves a (var sym) form to one yet, so there's still nothing real for the reader to hand back.

  • #_form datum 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 real Form.t() is nil/boolean/struct/number/binary/list/map/MapSet -- never a bare Elixir tuple). Every rule that assembles a form* 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/1 does 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 reified meta form onto target.meta when target is a %Logos.Symbol{} (symbols carry a meta field 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 needs Logos.Runtime state (ctx) during the parse itself, not just at the later desugar_syntax_quote/2 pass -- resolving a tag against Logos.Runtime's :data_readers registry has to happen at read time so '#inst "..." still yields the resolved value under quote. The registered reader is always a plain Elixir function reached through Logos.Interop.Allowlist (never an arbitrary Logos closure), called directly via Kernel.apply/3 -- this is still "pure reification calling a plain Elixir helper," the same category of thing Char.parse/1 already 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

desugar_syntax_quote(list, runtime)

@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).

discard()

The #_form discard sentinel -- exported so Logos.Reader can filter it out of read_all/1's top-level results too.