Ichor.Toolkit.Codegen (Ichor v0.2.0)

Copy Markdown View Source

Small, IR-agnostic quote/unquote mechanics -- not a code generator, and deliberately not a framework for building one (no tree-walker, no visitor dispatch, no assumed node shape, no assumed function-signature convention). Each function here solves exactly one mechanical pain point that recurs when hand-writing a compiler-to- quoted-Elixir backend, extracted from Grammar.Native's own codegen (CharCompiler, RuleCompiler, Native.LR) after the same handful of tricks turned up independently duplicated across (and within) those modules -- the same duplication signal that justified Ichor.Toolkit.Fixpoint.

Summary

Functions

The quoted AST for &name/arity, buildable at macro-expansion time even when name is a plain runtime atom -- &unquote(name)/unquote(arity) doesn't work directly through quote/unquote (the capture operator needs a literal function reference at the AST level, not a value to substitute). Duplicated verbatim in both Grammar.Native.CharCompiler and Grammar.Native.RuleCompiler before this existed -- one of their own moduledocs cross-references the other's copy.

Quotes shapes (an atom() => MapSet.t(atom()) map, e.g. Grammar.VM.RuleCompiler.capture_shapes/1's own return value) as AST that reconstructs each MapSet via MapSet.new/1, instead of Macro.escape/1's literal %MapSet{map: ...} struct pattern.

One pattern -> body clause, for splicing a dynamically-sized list of them into a case/cond/fn. A bare -> can't be written as a standalone quoted expression outside one of those three constructs (unlike, say, <- for a with clause, which is an ordinary operator usable anywhere) -- extracted from Grammar.Native.LR's own raw {:->, [], [[pattern], body]} construction for compiling per-state dispatch.

Like clause/2, but with a guard: pattern when guard -> body.

A fresh, collision-free atom built from prefix and counter, plus the next counter to use -- the "how do I name my Nth generated helper function" pattern (Grammar.Native.RuleCompiler's own fresh_name/1, Grammar.Native.CharCompiler's analog, both hardcoded to one specific prefix before this existed).

A list of count unhygienic variables sharing base_name, numbered from start (default 0) -- indexed_vars(:pos, 3, 1) gives pos1, pos2, pos3. Generalizes a pattern Grammar.Native.RuleCompiler's own Seq compilation already threads three separate instances of by hand (pos1, pos2, ..., ref1, ref2, ..., cap0, cap1, ...), one intermediate variable per sequence position.

A map of name => Macro.var(name, nil) for every name in names -- the unhygienic variable references (context: nil, not a fresh hygiene context per quote call) that let two separately-quoted function bodies refer to the same variable by name, needed whenever generated functions call each other and must agree on parameter names (stream, pos, context, ...). Generalizes the ad-hoc srp()-style helper Grammar.Native.RuleCompiler/CharCompiler each wrote their own copy of.

Functions

capture(name, arity)

@spec capture(atom(), non_neg_integer()) :: Macro.t()

The quoted AST for &name/arity, buildable at macro-expansion time even when name is a plain runtime atom -- &unquote(name)/unquote(arity) doesn't work directly through quote/unquote (the capture operator needs a literal function reference at the AST level, not a value to substitute). Duplicated verbatim in both Grammar.Native.CharCompiler and Grammar.Native.RuleCompiler before this existed -- one of their own moduledocs cross-references the other's copy.

capture_shapes_ast(shapes)

@spec capture_shapes_ast(%{optional(atom()) => MapSet.t(atom())}) :: Macro.t()

Quotes shapes (an atom() => MapSet.t(atom()) map, e.g. Grammar.VM.RuleCompiler.capture_shapes/1's own return value) as AST that reconstructs each MapSet via MapSet.new/1, instead of Macro.escape/1's literal %MapSet{map: ...} struct pattern.

MapSet.t() is only ever supposed to come from MapSet.new/1 and friends -- a literal struct pattern spelling out its internal :map representation (exactly what a bare Macro.escape/1 produces) reaches around that abstraction, which trips Dialyzer's opaqueness check on every module a grammar compiles to (Grammar.Native/.LR/.GLR all splice a grammar's own capture_shapes in as a literal this way).

clause(pattern, body)

@spec clause(Macro.t(), Macro.t()) :: Macro.t()

One pattern -> body clause, for splicing a dynamically-sized list of them into a case/cond/fn. A bare -> can't be written as a standalone quoted expression outside one of those three constructs (unlike, say, <- for a with clause, which is an ordinary operator usable anywhere) -- extracted from Grammar.Native.LR's own raw {:->, [], [[pattern], body]} construction for compiling per-state dispatch.

clause(pattern, guard, body)

@spec clause(Macro.t(), Macro.t(), Macro.t()) :: Macro.t()

Like clause/2, but with a guard: pattern when guard -> body.

fresh(prefix, counter)

@spec fresh(atom() | String.t(), non_neg_integer()) :: {atom(), non_neg_integer()}

A fresh, collision-free atom built from prefix and counter, plus the next counter to use -- the "how do I name my Nth generated helper function" pattern (Grammar.Native.RuleCompiler's own fresh_name/1, Grammar.Native.CharCompiler's analog, both hardcoded to one specific prefix before this existed).

indexed_vars(base_name, count, start \\ 0)

@spec indexed_vars(atom() | String.t(), non_neg_integer(), non_neg_integer()) :: [
  Macro.t()
]

A list of count unhygienic variables sharing base_name, numbered from start (default 0) -- indexed_vars(:pos, 3, 1) gives pos1, pos2, pos3. Generalizes a pattern Grammar.Native.RuleCompiler's own Seq compilation already threads three separate instances of by hand (pos1, pos2, ..., ref1, ref2, ..., cap0, cap1, ...), one intermediate variable per sequence position.

vars(names)

@spec vars([atom()]) :: %{required(atom()) => Macro.t()}

A map of name => Macro.var(name, nil) for every name in names -- the unhygienic variable references (context: nil, not a fresh hygiene context per quote call) that let two separately-quoted function bodies refer to the same variable by name, needed whenever generated functions call each other and must agree on parameter names (stream, pos, context, ...). Generalizes the ad-hoc srp()-style helper Grammar.Native.RuleCompiler/CharCompiler each wrote their own copy of.