crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Hoist complex element expressions outside container literals

Open HertzDevil opened this issue 2 years ago • 6 comments

Fixes #7340. A "complex expression" is anything that isn't a Nop, NilLiteral, BoolLiteral, NumberLiteral, CharLiteral, StringLiteral, SymbolLiteral, Var, InstanceVar, or ClassVar. (This same criterion is used by the normalizer to determine whether the LHS of a compound assignment needs to be hoisted into a temporary variable.) As the examples below show, those complex expressions cannot appear inside the expanded typeof expressions.

The following:

class Array(T)
  def self.unsafe_build(capacity); new; end
  def to_unsafe; self; end
  def []=(index, value); end
end

[[[nil]]]

produces with AFTER=1 crystal build --prelude=empty:

Before

After
__temp_1 = (
  __temp_4 = (
    __temp_7 = ::Array(typeof(nil)).unsafe_build(1)
    __temp_8 = __temp_7.to_unsafe
    __temp_8[0] = nil
    __temp_7
  )
  __temp_5 = ::Array(typeof(__temp_4)).unsafe_build(1)
  __temp_6 = __temp_5.to_unsafe
  __temp_6[0] = __temp_4
  __temp_5
)
__temp_2 = ::Array(typeof(__temp_1)).unsafe_build(1)
__temp_3 = __temp_2.to_unsafe
__temp_3[0] = __temp_1
__temp_2

The following:

class Hash(K, V)
  def []=(index, value); end
end

{ { {'a' => 'b'} => 'c'} => {'d' => {'e' => 'f'} } }

produces:

Before
__temp_1 = ::Hash(typeof(begin
  __temp_2 = ::Hash(typeof(begin
    __temp_3 = ::Hash(typeof('a'), typeof('b')).new
    __temp_3['a'] = 'b'
    __temp_3
  end), typeof('c')).new
  __temp_2[begin
    __temp_4 = ::Hash(typeof('a'), typeof('b')).new
    __temp_4['a'] = 'b'
    __temp_4
  ] = 'c'
  __temp_2
end, typeof(begin
  __temp_5 = ::Hash(typeof('d'), typeof(begin
    __temp_6 = ::Hash(typeof('e'), typeof('f')).new
    __temp_6['e'] = 'f'
    __temp_6
  end)).new
  __temp_5['d'] = begin
    __temp_7 = ::Hash(typeof('e'), typeof('f')).new
    __temp_7['e'] = 'f'
    __temp_7
  end
  __temp_5
end)).new
__temp_1[begin
  __temp_8 = ::Hash(typeof(begin
    __temp_9 = ::Hash(typeof('a'), typeof('b')).new
    __temp_9['a'] = 'b'
    __temp_9
  end), typeof('c')).new
  __temp_8[begin
    __temp_10 = ::Hash(typeof('a'), typeof('b')).new
    __temp_10['a'] = 'b'
    __temp_10
  end] = 'c'
  __temp_8
end] = begin
  __temp_11 = ::Hash(typeof('d'), typeof(begin
    __temp_12 = ::Hash(typeof('e'), typeof('f')).new
    __temp_12['e'] = 'f'
    __temp_12
  end)).new
  __temp_11['d'] = begin
    __temp_13 = ::Hash(typeof('e'), typeof('f')).new
    __temp_13['e'] = 'f'
    __temp_13
  end
  __temp_11
end
__temp_1
After
__temp_1 = (
  __temp_4 = (
    __temp_6 = ::Hash(typeof('a'), typeof('b')).new
    __temp_6['a'] = 'b'
    __temp_6
  )
  __temp_5 = ::Hash(typeof(__temp_4), typeof('c')).new
  __temp_5[__temp_4] = 'c'
  __temp_5
)
__temp_2 = (
  __temp_7 = (
    __temp_9 = ::Hash(typeof('e'), typeof('f')).new
    __temp_9['e'] = 'f'
    __temp_9
  )
  __temp_8 = ::Hash(typeof('d'), typeof(__temp_7)).new
  __temp_8['d'] = __temp_7
  __temp_8
)
__temp_3 = ::Hash(typeof(__temp_1), typeof(__temp_2)).new
__temp_3[__temp_1] = __temp_2
__temp_3

HertzDevil avatar Aug 07 '22 00:08 HertzDevil

Is this a relevant question for code using the Type{} shortcut (ie Set{1, 2, Deque{1,2}}) as well?

(I sincerely hope no-one writes code like that particular example)

yxhuvud avatar Aug 07 '22 14:08 yxhuvud

Array-like and hash-like literals are also covered. Before:

__temp_1 = Set(T)(typeof(1, 2, begin
  __temp_2 = Deque(T)(typeof(1, 2)).new
  __temp_2 << 1
  __temp_2 << 2
  __temp_2
end)).new
__temp_1 << 1
__temp_1 << 2
__temp_1 << (
  __temp_3 = Deque(T)(typeof(1, 2)).new
  __temp_3 << 1
  __temp_3 << 2
  __temp_3
)
__temp_1

After:

__temp_1 = (
  __temp_3 = Deque(T)(typeof(1, 2)).new
  __temp_3 << 1
  __temp_3 << 2
  __temp_3
)
__temp_2 = Set(T)(typeof(1, 2, __temp_1)).new
__temp_2 << 1
__temp_2 << 2
__temp_2 << __temp_1
__temp_2

HertzDevil avatar Aug 07 '22 14:08 HertzDevil

A more realistic example, this nested container in the standard library specs:

https://github.com/crystal-lang/crystal/blob/5de237e052004bca0e5a43e903139558f5368a2a/spec/std/crypto/subtle_spec.cr#L6-L11

Before
__temp_1 = ::Array(typeof(begin
  __temp_3 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 17), Slice.new(1, 17), true)).new
  __temp_3["a"] = Slice.new(1, 17)
  __temp_3["b"] = Slice.new(1, 17)
  __temp_3["result"] = true
  __temp_3
end, begin
  __temp_6 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 18), Slice.new(1, 17), false)).new
  __temp_6["a"] = Slice.new(1, 18)
  __temp_6["b"] = Slice.new(1, 17)
  __temp_6["result"] = false
  __temp_6
end, begin
  __temp_7 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 17), Slice.new(2) do |i|
    17 + i
  end, false)).new
  __temp_7["a"] = Slice.new(1, 17)
  __temp_7["b"] = Slice.new(2) do |i|
    17 + i
  end
  __temp_7["result"] = false
  __temp_7
end, begin
  __temp_10 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(2) do |i|
    17 + i
  end, Slice.new(1, 17), false)).new
  __temp_10["a"] = Slice.new(2) do |i|
    17 + i
  end
  __temp_10["b"] = Slice.new(1, 17)
  __temp_10["result"] = false
  __temp_10
end)).unsafe_build(4)
__temp_2 = __temp_1.to_unsafe
__temp_2[0] = begin
  __temp_13 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 17), Slice.new(1, 17), true)).new
  __temp_13["a"] = Slice.new(1, 17)
  __temp_13["b"] = Slice.new(1, 17)
  __temp_13["result"] = true
  __temp_13
end
__temp_2[1] = begin
  __temp_14 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 18), Slice.new(1, 17), false)).new
  __temp_14["a"] = Slice.new(1, 18)
  __temp_14["b"] = Slice.new(1, 17)
  __temp_14["result"] = false
  __temp_14
end
__temp_2[2] = begin
  __temp_15 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(1, 17), Slice.new(2) do |i|
    17 + i
  end, false)).new
  __temp_15["a"] = Slice.new(1, 17)
  __temp_15["b"] = Slice.new(2) do |i|
    17 + i
  end
  __temp_15["result"] = false
  __temp_15
end
__temp_2[3] = begin
  __temp_18 = ::Hash(typeof("a", "b", "result"), typeof(Slice.new(2) do |i|
    17 + i
  end, Slice.new(1, 17), false)).new
  __temp_18["a"] = Slice.new(2) do |i|
    17 + i
  end
  __temp_18["b"] = Slice.new(1, 17)
  __temp_18["result"] = false
  __temp_18
end
__temp_1
After
__temp_1 = (
  __temp_7 = Slice.new(1, 17)
  __temp_8 = Slice.new(1, 17)
  __temp_9 = ::Hash(typeof("a", "b", "result"), typeof(__temp_7, __temp_8, true)).new
  __temp_9["a"] = __temp_7
  __temp_9["b"] = __temp_8
  __temp_9["result"] = true
  __temp_9
)
__temp_2 = (
  __temp_12 = Slice.new(1, 18)
  __temp_13 = Slice.new(1, 17)
  __temp_14 = ::Hash(typeof("a", "b", "result"), typeof(__temp_12, __temp_13, false)).new
  __temp_14["a"] = __temp_12
  __temp_14["b"] = __temp_13
  __temp_14["result"] = false
  __temp_14
)
__temp_3 = (
  __temp_15 = Slice.new(1, 17)
  __temp_16 = Slice.new(2) do |i|
    17 + i
  end
  __temp_17 = ::Hash(typeof("a", "b", "result"), typeof(__temp_15, __temp_16, false)).new
  __temp_17["a"] = __temp_15
  __temp_17["b"] = __temp_16
  __temp_17["result"] = false
  __temp_17
)
__temp_4 = (
  __temp_19 = Slice.new(2) do |i|
    17 + i
  end
  __temp_20 = Slice.new(1, 17)
  __temp_21 = ::Hash(typeof("a", "b", "result"), typeof(__temp_19, __temp_20, false)).new
  __temp_21["a"] = __temp_19
  __temp_21["b"] = __temp_20
  __temp_21["result"] = false
  __temp_21
)
__temp_5 = ::Array(typeof(__temp_1, __temp_2, __temp_3, __temp_4)).unsafe_build(4)
__temp_6 = __temp_5.to_unsafe
__temp_6[0] = __temp_1
__temp_6[1] = __temp_2
__temp_6[2] = __temp_3
__temp_6[3] = __temp_4
__temp_5

HertzDevil avatar Aug 08 '22 09:08 HertzDevil

I sincerely hope no-one writes code like that particular example

No matter what language feature we are talking about, in my experience hoping that someone won't do this or that is always in vain 😄

asterite avatar Aug 08 '22 12:08 asterite

This is really cool!

One optimization I always wanted to do, but didn't think it's worth it (but it probably is!) is trying to not use typeof when we know the type of all elements. For example, if we have [1, 2] we can just do Array(Int32). Same if we have [1, nil], we can do Array(Int32 | Nil). And even in something like [[1,2]], we can do Array(Array(Int32)) and avoid using typeof.

But that can come as a follow-up PR.

asterite avatar Aug 08 '22 12:08 asterite

Some expansions are actually longer when they don't involve typeof expressions, i.e. when the container literals use of and the container-like literals don't use uninstantiated generics. Take this example from the parser specs:

[(ArrayLiteral.new([(ArrayLiteral.new([1.int32] of ASTNode)).splat] of ASTNode)).splat, (ArrayLiteral.new([2.int32] of ASTNode)).splat] of ASTNode

Before:

__temp_1 = ::Array(ASTNode).unsafe_build(2)
__temp_2 = __temp_1.to_unsafe
__temp_2[0] = (ArrayLiteral.new(begin
  __temp_3 = ::Array(ASTNode).unsafe_build(1)
  __temp_4 = __temp_3.to_unsafe
  __temp_4[0] = (ArrayLiteral.new(begin
    __temp_5 = ::Array(ASTNode).unsafe_build(1)
    __temp_6 = __temp_5.to_unsafe
    __temp_6[0] = 1.int32
    __temp_5
  end)).splat
  __temp_3
end)).splat
__temp_2[1] = (ArrayLiteral.new(begin
  __temp_8 = ::Array(ASTNode).unsafe_build(1)
  __temp_9 = __temp_8.to_unsafe
  __temp_9[0] = 2.int32
  __temp_8
end)).splat
__temp_1

After:

__temp_1 = (ArrayLiteral.new(begin
  __temp_5 = (ArrayLiteral.new(begin
    __temp_8 = 1.int32
    __temp_9 = ::Array(ASTNode).unsafe_build(1)
    __temp_10 = __temp_9.to_unsafe
    __temp_10[0] = __temp_8
    __temp_9
  end)).splat
  __temp_6 = ::Array(ASTNode).unsafe_build(1)
  __temp_7 = __temp_6.to_unsafe
  __temp_7[0] = __temp_5
  __temp_6
end)).splat
__temp_2 = (ArrayLiteral.new(begin
  __temp_12 = 2.int32
  __temp_13 = ::Array(ASTNode).unsafe_build(1)
  __temp_14 = __temp_13.to_unsafe
  __temp_14[0] = __temp_12
  __temp_13
end)).splat
__temp_3 = ::Array(ASTNode).unsafe_build(2)
__temp_4 = __temp_3.to_unsafe
__temp_4[0] = __temp_1
__temp_4[1] = __temp_2
__temp_3

One can spot that after this PR the 3 calls to #int32 and the 2 calls to #splat are hoisted, which are where the extra temporary variables come from. However I still think we should do this and not selectively apply the optimization to the other kinds of container literals, because this ensures a consistent execution order across all those containers.

HertzDevil avatar Aug 08 '22 12:08 HertzDevil