diff --git a/spec/compiler/macro/macro_expander_spec.cr b/spec/compiler/macro/macro_expander_spec.cr index eab2e6d98785..4a17f3f483ce 100644 --- a/spec/compiler/macro/macro_expander_spec.cr +++ b/spec/compiler/macro/macro_expander_spec.cr @@ -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" @@ -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 diff --git a/src/compiler/crystal/macros/interpreter.cr b/src/compiler/crystal/macros/interpreter.cr index 4b74492d29f6..f0444e2f4864 100644 --- a/src/compiler/crystal/macros/interpreter.cr +++ b/src/compiler/crystal/macros/interpreter.cr @@ -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