The Crystal Programming Language Forum

Macro type help

is_a doesn’t work like I thought. Is there a macro version that will resolve the two errors below?

Each variable may be Bytes? or StaticArray(UInt8).

    {% for ivar in @type.instance_vars %}
      {% if ann = ivar.annotation(Wipe::Var) %}
        if @{{ ivar.id }}.is_a?(StaticArray)
puts "wiping static {{ivar}}"
# Error: undefined method 'to_slice' for Nil (compile-time type is (Slice(UInt8) | Nil))
            Sodium.memzero @{{ ivar.id }}.to_slice
        else
          if var = @{{ ivar.id }}
# Error: no overload matches 'Sodium.memzero' with type StaticArray(UInt8,
            Sodium.memzero var
          end
        end
      {% end %}
    {% end %}

You could prob add that logic to the macro code. Something like

{% for ivar in @type.instance_vars %}
  {% if ann = ivar.annotation(Wipe::Var) %}
    {% if ivar.type.id == StaticArray.id %}
      puts "wiping static {{ivar}}"
      Sodium.memzero @{{ ivar.id }}.to_slice
    {% else %}
      if var = @{{ ivar.id }}
        Sodium.memzero var
      end
    {% end %}
  {% end %}
{% end %}

Doing type comparisons in macro land can be kinda a pain.

Another option is to use a case.

{% for ivar in @type.instance_vars %}
  {% if ann = ivar.annotation(Wipe::Var) %}
    val = @{{ivar.id}}
    return unless val
    case val
    when StaticArray then Sodium.memzero val.to_slice
    else
      Sodium.memzero var
    end
  {% end %}
{% end %}

You can’t restrict the type of an instance variable. Assign it first to a local variable.

If I assign to a local variable StaticArray is copied defeating the purpose.

class C
        @a = StaticArray(UInt8, 4).new 0

        def b
                b = @a.to_slice
                b[1] = 1_u8

                c = @a
                c.to_slice
                c[2] = 2_u8
        end
end

c = C.new

c.b
p c

The second modification doesn’t modify the original StaticArray.
Output:

#<C:0x10c19eff0 @a=StaticArray[0, 1, 0, 0]>

You could assign it back to the ivar.
Or maybe you can try luck with pointer to struct.

Or, if this is in the initializer, and you are 100% sure about the type, a call to not_nil! might be helpful to ensure the is well, not nil. A similar thing can be done with as to check the type. But you are opening the door to runtime cast errors.

@bcardiff The main problem is if v.is_a?(StaticArray). In either case the compiler thinks it can be both types and never compiles. The comments in my first post show the errors.

My latest attempt combining a switch statement and assigning back to the ivar crashes. It’s not the memzero, but the assignment crashing it.

    {% for ivar in @type.instance_vars %}
      {% if ann = ivar.annotation(Wipe::Var) %}
        if var = @{{ ivar.id }}
          case var
          when StaticArray
puts "wiping {{ivar}}"
#            Sodium.memzero var.to_slice
            @{{ ivar.id }} = var
          else
            Sodium.memzero var.to_slice
          end
        end
      {% end %}
    {% end %}
Invalid memory access (signal 11) at address 0x4
[0x105a6eb1b] *CallStack::print_backtrace:Int32 +107
[0x105a47fb5] __crystal_sigfault_handler +181
[0x7fff5f1d9b5d] _sigtramp +29
[0x105ae3184] *Sodium::Digest::Blake2b@Sodium::Wipe#wipe:(StaticArray(UInt8, 64) | Nil) +52
[0x105ae3097] *Sodium::Digest::Blake2b@Sodium::Wipe#finalize:(StaticArray(UInt8, 64) | Nil) +55
[0x105a499c7] ~procProc(Pointer(Void), Pointer(Void), (StaticArray(UInt8, 64) | Nil))@/usr/local/Cellar/crystal/0.28.0/src/gc/boehm.cr:130 +55
[0x105b208fe] GC_invoke_finalizers +146
[0x105b20a2e] GC_notify_or_invoke_finalizers +150
[0x105b1c55d] GC_try_to_collect_general +254
[0x105b1c591] GC_gcollect +13
[0x105a70729] *GC::collect:Nil +9
[0x105a4e571] *check_wiped<Slice(UInt8)>:Nil +49

You could check the type directly in the macro code.

Also, it’s easier to help if you give us the full code. Otherwise we can only guess.

https://github.com/didactic-drunk/sodium.cr/blob/master/src/sodium/wipe.cr

Another alternative: use is_a? on an instance var, then use .as(...) inside the branch. It’s an extra type assertion but it will save you from an extra copy. However, note that StaticArray is always passed by copy so I’m not sure what you are doing there with StaticArray will work.

This seems to work:

  protected def wipe
    return if @closed

    {% for ivar in @type.instance_vars %}
      {% if ann = ivar.annotation(Wipe::Var) %}
        {% if ivar.type <= StaticArray %}
          Sodium.memzero @{{ivar.id}}.to_slice
        {% elsif ivar.type <= Slice %}
          Sodium.memzero @{{ivar.id}}
        {% end %}
      {% end %}
    {% end %}
  end
2 Likes

I tried:

{% if ivar.type == StaticArray %}

Instead of:

{% if ivar.type <= StaticArray %}

Why does <= work?

The first is comparing the ivar’s type (with generics) to the type StaticArray (without generics).

StaticArray(Int32, 3) == StaticArray # => false
StaticArray(Int32, 3) <= StaticArray # => true

<= makes the comparison be like "if the ivar’s type includes or is a child of StaticArray" which it is.

1 Like

Why doesn’t this work?

{% for ivar in @type.instance_vars %}
  {% if ivar.is_a? StaticArray %}

It’s because is_a? inside macro code checks against the AST node type. What you can put there is Path, Generic, ClassDef, etc. Maybe it’s not a bit intuitive, and it might be a good idea to be able to do ivar.type.is_a?(...).

Would it be more clear if is_a? worked in crystal and macros the same way with the ast node using is_ast? is_an_ast? ast? ast_type? ?

Well, it already works the same as in runtime code: variables and things in crystal macros are always ast nodes, and with is_a? you are actually asking “is it this type of node?”.

class C
  @a = "a"
  @b = StaticArray(UInt8, 384).new 0

  def c
    {% for ivar in @type.instance_vars %}
      val = @{{ ivar.id }}
      p val.is_a?(String)
      {% if ivar.is_a? String %}
        puts "true"
      {% else %}
        puts "false"
      {% end %}
    {% end %}
  end
end

C.new.c

Output:

true
false
false
false

Expected Output:

true
false
true
false

I understand what you say, but imagine we eventually implement macros by compiling them into Crystal programs. The type of ivar will be some kind of Var, not a String. In fact, there are no Strings in macro land, only StringLiteral instances. Only AST nodes are manipulated in macros, and AST nodes are the ones under this tree: https://crystal-lang.org/api/0.29.0/Crystal/Macros.html