Is there a way to see what a Crystal macro expands to? - macros

I've a macro that refuses to work as expected and I was wondering if there was a way to see what it expands to, is there something like macroexpand-1 from lisp in Crystal? If so, how do I use it? Thanks!

Placing {% debug %} at the end of the macro will print it's contents at compile time.
e.g.
macro foo
...
{% debug %}
end

Appears another tool is crystal tool expand
ex file mapping_test.cr
require "json"
require "./json_mapping"# wget https://raw.githubusercontent.com/crystal-lang/json_mapping.cr/master/src/json_mapping.cr
class Location
JSON.mapping( # <---- line: 4, column: 3
lat: Float64,
lng: Float64,
)
end
run it like this:
crystal tool expand -c mapping_test.cr:4:3 mapping_test.cr (must be right location or within the macro at least, or will get "no expansion found"
output
1 expansion found
expansion 1:
JSON.mapping(lat: Float64, lng: Float64)
# expand macro 'JSON.mapping' (/home/a/json_mapping.cr:236:3)
~> ::JSON.mapping({lat: {type: Float64, key_id: lat}, lng: {type: Float64, key_id: lng}})
...
ref https://groups.google.com/g/crystal-lang/c/L7ADzhRQGLk

Related

Why does one of these semingly equivalent macros fail?

Consider these two macro definitions:
macro createTest1()
quote
function test(a = false)
a
end
end |> esc
end
macro createTest2()
args = :(a = false)
quote
function test($args)
a
end
end |> esc
end
According to the builtin Julia facilities they should both evaluate to the same thing when expanded:
println(#macroexpand #createTest1)
begin
function test(a=false)
a
end
end
println(#macroexpand #createTest2)
begin
function test(a = false)
a
end
end
Still I get a parse error when trying to evaluate the second macro:
#createTest2
ERROR: LoadError: syntax: "a = false" is not a valid function argument name
It is a space in the second argument list. However, that should be correct Julia syntax. My guess is that it interprets the second argument list as another Julia construct compared to the first. If that is the case how do I get around it?
The reason that the second macro is failing as stated in my question above. It looks correct when printed however args is not defined correctly and Julia interprets it as an expression which is not allowed. The solution is to instead define args according to the rules for function parameters. The following code executes as expected:
macro createTest2()
args = Expr(:kw, :x, false)
quote
function test($(args))
a
end
end |> esc
end

Passing computed list to an Elixir macro

I have a map that I want to use a single source of truth for a couple of functions. Let's say it is:
source_of_truth = %{a: 10, b: 20}
I'd like the keys of that map to be values of EctoEnum. EctoEnum provides a macro defenum that I should use like this:
defenum(
EnumModule,
:enum_name,
[:a, :b]
)
I don't want to repeat [:a, :b] part. I'd like to use the keys from the map instead like this:
defenum(
EnumModule,
:enum_name,
Map.keys(source_of_truth)
)
It doesn't work because defenum macro expects a plain list.
I thought I could do it by defining my own macro like this:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(enum_values)
)
end
end
and then call:
dynamic_enum(EnumModule, :enum_name, Map.keys(source_of_truth))
However, it doest the same thing: enum_values is not a precomputed list but AST for Map.get. My next approach was:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
values = unquote(enum_values)
defenum(
unquote(enum_module),
unquote(enum_name),
?
)
end
end
Not sure what I could put where the ? is. I can't just put values because it is a variable and not a list. I can't put unquote(values) either.
A solution that sort of works is this one:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
{values, _} = Code.eval_quoted(enum_values)
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(values)
)
end
end
However, the docs say that using eval_quoted inside a macro is a bad practice.
[EDIT]
A solution with Macro.expand does not work either because it doesn't actually evaluate anything. The expansion stops at:
Expanded: {{:., [],
[
{:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
:get_attribute
]}, [],
[
{:__MODULE__, [counter: -576460752303357631], Kernel},
:keys,
[
{:{}, [],
[
TestModule,
:__MODULE__,
0,
[
file: '...',
line: 16
]
]}
]
]}
So it does not expand to the list as we expected.
[\EDIT]
What is a good solution for that problem?
As stated in the documentation for Macro.expand/2
The following contents are expanded:
Macros (local or remote)
Aliases are expanded (if possible) and return atoms
Compilation environment macros (__CALLER__/0, __DIR__/0, __ENV__/0 and __MODULE__/0)
Module attributes reader (#foo)
Emphasis is mine. So the possibility would be to use module attributes with Macro.expand/2.
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
And call it like:
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
FWIW, the full code:
$ \cat lib/eenum.ex
defmodule Eenum do
import EctoEnum
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
end
$ \cat lib/tester.ex
defmodule Tester do
import Eenum
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, #keys)
end
end
FWIW 2. To be able to call dynamic_enum as shown above from the module scope, all you need is (surprise :) another module scope, already compiled at the moment of macro invocation:
defmodule Defs do
#source_of_truth %{a: 10, b: 20}
#keys Map.keys(#source_of_truth)
defmacro keys, do: Macro.expand(#keys, __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
FWIW 3. The latter (explicit module with definitions) will work even without a necessity to have module attributes:
defmodule Defs do
defmacro keys, do: Macro.expand(Map.keys(%{a: 10, b: 20}), __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
The rule of thumb is when you find yourself in a need to invoke Code.eval_quoted/3, put this code into the independent module and make compiler invoke this code compilation for you. For functions is works on the module level, for module level it should be put into another module to make module context (aka __CALLER__ and __ENV__) available.
I battled with the same problem a while back. Basically you can build your syntax tree in a quote, using unquote to inject your dynamic value, and then use Code.eval_quoted to eval the macros:
options = Map.keys(source_of_truth)
Code.eval_quoted(
quote do
EctoEnum.defenum(MyEnum, :type_name, unquote(options))
end,
[],
__ENV__
)

Elixir Macro issue with unquoting

Can you please have a look at my Macro?
I am getting undefined function number/0 error, and I can't figure it out why.
defmodule DbUtil do
defmacro __using__(opts) do
quote do
import unquote(__MODULE__)
#before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(%{module: definition} = _env) do
quote do
import Ecto.Query
def last do
from x in unquote(definition), order_by: [desc: x.id], limit: 1
end
# This dumps error
def limits(number) do
from a in unquote(definition), limit: ^unquote(number)
end
end
end
end
You don't need to unquote number. unquote is used when you want to inject a variable present outside the quote block. Since number is defined inside the quote, you don't need to unquote. The following should work for you:
def limits(number) do
from a in unquote(definition), limit: ^number
end

Variable labels in SPSS Macro

I'm new to the SPSS macro syntax and had a hard time trying to label variables based on a simple loop counter. Here's what I tried to do:
define !make_indicatorvars()
!do !i = 1 !to 10.
!let !indicvar = !concat('indexvar_value_', !i, '_ind')
compute !indicvar = 0.
if(indexvar = !i) !indicvar = 1.
variable labels !indicvar 'Indexvar has value ' + !quote(!i).
value labels !indicvar 0 "No" 1 "Yes".
!doend
!enddefine.
However, when I run this, I get the following warnings:
Warning # 207 on line ... in column ... Text: ...
A '+' was found following a text string, indicating continuation, but the next non-blank character was not a quotation mark or an apostrophe.
Warning # 4465 in column ... Text: ...
An invalid symbol appears on the VAR LABELS command where a slash was
expected. All text through the next slash will be be ignored.
Indeed the label is then only 'Indexvar has value '.
Upon using "set mprint on printback on", the following code was printed:
variable labels indexvar_value_1_ind 'Indexvar has value ' '1'
So it appears that SPSS seems to somehow remove the "+" which is supposed to concatenate the two strings, but why?
The rest of the macro worked fine, it's only the variable labels command that's causing problems.
Try:
variable labels !indicvar !quote(!concat('Indexvar has value ',!i)).
Also note:
compute !indicvar = 0.
if(indexvar = !i) !indicvar = 1.
Can be simplified as:
compute !indicvar = (indexvar = !i).
Where the right hand side of the COMPUTE equation evaluates to equal TRUE a 1 (one) is assigned else if FALSE a 0 (zero) is assigned. Using just a single compute in this way not only reduce the lines of code, it will also make the transformations more efficient/quicker to run.
You might consider the SPSSINC CREATE DUMMIES extension command. It will automatically construct a set of dummies for a variable and label them with the values or value labels. It also creates a macro that lists all the variables. There is no need to enumerate the values. It creates dummies for all the values in the data.
It appears on the Transform menu as long as the Python Essentials are installed. Here is an example using the employee data.sav file shipped with Statistics.
SPSSINC CREATE DUMMIES VARIABLE=jobcat
ROOTNAME1=job
/OPTIONS ORDER=A USEVALUELABELS=YES USEML=YES OMITFIRST=NO
MACRONAME1="!jobcat".

Elixir quote record(Turn it into a tuple) and preserve data?

When quoted using quote do: records aren't converted to tuples containing the record fields:
iex(1)> quote do: is_bitstring("blah")
{:is_bitstring, [context: Elixir, import: Kernel], ["blah"]}
iex(2)> quote do: Computer.new("Test")
{{:., [], [{:__aliases__, [alias: false], [:Computer]}, :new]}, [], [[name: "Test"]]}
iex(3)> quote do: Computer.new("Test")
{{:., [], [{:__aliases__, [alias: false], [:Computer]}, :new]}, [], [[name: "Test"]]}
iex(4)> c = Computer.new("Test")
Computer[name: "Test", type: nil, processor: nil, hard_drives: []]
iex(5)> c
Computer[name: "Test", type: nil, processor: nil, hard_drives: []]
iex(6)> quote do: c
{:c, [], Elixir}
Also, when I try doing this in my code:
defmacro computer([do: code]) do
# macro login here
# build computer record based on macro logic
computer = Computer.new(params)
quote do: unquote computer
end
I get an error:
** (CompileError) elixir/test/lib/computer_dsl_test.exs: tuples in quoted expressions must have 2 or 3 items, invalid quoted expression: Computer[name: "", type: nil, processor: nil, hard_drives: []]
I thought that records were just tuples with wrappers functions of some sort. The Elixir Getting Started guide states "A record is simply a tuple where the first element is the record module name." Is there something I am missing? Is there a function I can call on a record to get the tuple representation? I am aware of the raw: true option but I am not sure how to use that on an existing record.
Any insights?
Records are tuples. The output you see on the console is just formatted for easier inspection. You can check that records are tuples if you inspect them with raw: true:
iex(1)> defrecord X, a: 1, b: 2
iex(2)> x = X.new
X[a: 1, b: 2] # This is formatted output. x is really a tuple
iex(3)> IO.inspect x, raw: true
{X, 1, 2}
As can be seen, a record instance is really a tuple. You can also pattern match on it (although I don't recommend this):
iex(4)> {a, b, c} = x
iex(8)> a
X
iex(9)> b
1
iex(10)> c
2
The quote you are mentioning serves completely different purpose. It turns an Elixir expression into AST representation that can be injected into the rest of the AST, most often from the macro. Quote is relevant only in compile time, and as such, it can't even know what is in your variable. So when you say:
quote do: Computer.new("Test")
The result you get is AST representation of the call of the Computer.new function. But the function is not called at this point.
Just reading the error message and the elixir "getting stated" on macro definition it appears that the result of a quote has the form:
In general, each node (tuple) above follows the following format:
{ tuple | atom, list, list | atom }
The first element of the tuple is an atom or another tuple in the same representation;
The second element of the tuple is an list of metadata, it may hold information like the node line number;
The third element of the tuple is either a list of arguments for the function call or an atom. When an atom,
it means the tuple represents a variable.
Besides the node defined above, there are also five Elixir literals that when quoted return themselves (and not a tuple). They are:
:sum #=> Atoms
1.0 #=> Numbers
[1,2] #=> Lists
"binaries" #=> Strings
{key, value} #=> Tuples with two elements
My guess is that the unquote is the reverse function of quote, and so it expects as argument one of the above forms. This is not the case for the computer record.
I think the unquote is not necessary there (although I didn't try to understand the intent of your code...) and that
defmacro computer([do: code]) do %% why do you need this argument?
quote do: Computer.new
end
should be ok.