Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions spec/compiler/macro/macro_expander_spec.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
require "../../spec_helper"

private STABLE_ABI_TYPES = [
Nil, Void, Int32, Float32, Bool, Char, Symbol, Pointer(Void), # primitive scalars
StaticArray(Int32, 1), Tuple(Int32), NamedTuple(x: Int32), Proc(Int32), # primitive aggregates
Reference, String,
Union(Int32, Char), Union(Int32, String), Union(Nil, String), Union(Nil, String, Regex), Union(Nil, Proc(Int32)), # unions
]

private UNSTABLE_ABI_TYPES = [
Value, Struct, Int, Float,
Slice, Slice(Int32), Enumerable, Enumerable(Int32), Flags,
StaticArray(Slice(Int32), 1), Tuple(Slice(Int32)), NamedTuple(x: Slice(Int32)),
Union(Slice(Int32), Nil),
]

describe "MacroExpander" do
it "expands simple macro" do
assert_macro "1 + 2", "1 + 2"
Expand Down Expand Up @@ -170,4 +184,72 @@ describe "MacroExpander" do
] of Lexer::LocPragma,
}
end

{% for op in ["sizeof".id, "alignof".id] %}
describe "{{ op }}" do
{% for type in STABLE_ABI_TYPES %}
it "gets {{ op }} {{ type.id }}" do
# we are not interested in the actual sizes or alignments here, these
# values are up to the codegen spec suite
assert_macro %(\{{ {{ op }}({{ type.id }}).is_a?(NumberLiteral) }}), "true"
end
{% end %}

it "gets {{ op }} enum" do
assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
program.types["Foo"] = EnumType.new(program, program, "Foo", program.int16)
nil
end
end

it "gets {{ op }} alias" do
assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
program.types["Foo"] = AliasType.new(program, program, "Foo", Crystal::Path.global("Int16"))
nil
end
end

it "gets {{ op }} typedef" do
assert_macro("\{{ {{ op }}(Foo) == {{ op }}(Int16) }}", "true") do |program|
program.types["Foo"] = TypeDefType.new(program, program, "Foo", program.int16)
nil
end
end

it "errors with typeof" do
assert_error %(\{{ {{ op }}(typeof(1)) }})
end

{% for type in UNSTABLE_ABI_TYPES %}
it "errors with {{ type.id }}" do
assert_error %(\{{ {{ op }}({{ type.id }}) }}), "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
end
{% end %}

it "errors with alias of unstable type" do
assert_error <<-CRYSTAL, "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
struct Foo
end

alias Bar = Foo

\{{ {{ op }}(Bar) }}
CRYSTAL
end

it "errors with typedef of unstable type" do
assert_error <<-CRYSTAL, "argument to `{{ op }}` inside macros must be a type with a stable {{ op == "sizeof" ? "size".id : "alignment".id }}"
lib Lib
struct Foo
x : Int32
end

type Bar = Foo
end

\{{ {{ op }}(Lib::Bar) }}
CRYSTAL
end
end
{% end %}
end
64 changes: 64 additions & 0 deletions src/compiler/crystal/macros/interpreter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,70 @@ module Crystal
node.raise "can't resolve #{node} (#{node.class_desc})"
end

def visit(node : SizeOf)
type_node = resolve(node.exp)
unless type_node.is_a?(TypeNode) && stable_abi?(type_node.type)
node.raise "argument to `sizeof` inside macros must be a type with a stable size"
end

@last = NumberLiteral.new(@program.size_of(type_node.type.sizeof_type).to_i32)
false
end

def visit(node : AlignOf)
type_node = resolve(node.exp)
unless type_node.is_a?(TypeNode) && stable_abi?(type_node.type)
node.raise "argument to `alignof` inside macros must be a type with a stable alignment"
end

@last = NumberLiteral.new(@program.align_of(type_node.type.sizeof_type).to_i32)
false
end

# Returns whether *type*'s size and alignment are stable with respect to
# source code augmentation, i.e. they remain unchanged at the top level even
# as new code is being processed by the compiler at various phases.
#
# `instance_sizeof` and `instance_alignof` are inherently unstable, as they
# only work on subclasses of `Reference`, and instance variables can be
# added to them at will.
#
# This method does not imply there is a publicly stable ABI yet!
private def stable_abi?(type : Type) : Bool
case type
when ReferenceStorageType
# instance variables may be added at will
false
when GenericType, AnnotationType
# no such values exist
false
when .module?
# ABI-equivalent to the union of all including types, which may be added
# at will
false
when ProcInstanceType, PointerInstanceType
true
when StaticArrayInstanceType
stable_abi?(type.element_type)
when TupleInstanceType
type.tuple_types.all? { |t| stable_abi?(t) }
when NamedTupleInstanceType
type.entries.all? { |entry| stable_abi?(entry.type) }
when InstanceVarContainer
# instance variables of structs may be added at will; references always
# have the size and alignment of a pointer
!type.struct?
when UnionType
type.union_types.all? { |t| stable_abi?(t) }
when TypeDefType
stable_abi?(type.typedef)
when AliasType
stable_abi?(type.aliased_type)
else
true
end
end

def visit(node : Splat)
warnings.add_warning(node, "Deprecated use of splat operator. Use `#splat` instead")
node.exp.accept self
Expand Down
Loading