@@ -141,6 +141,47 @@ defmodule Lua.VM.StringTest do
141141 state = Stdlib . install ( State . new ( ) )
142142 assert { :ok , [ "" ] , _state } = VM . execute ( proto , state )
143143 end
144+
145+ # Building the repeated string must allocate proportionally to the result,
146+ # not to the repeat count. The previous `Enum.map_join(1..n, ...)`
147+ # implementation materialized an n-element list (tens of times the result
148+ # size) as transient garbage, spiking the heap and tripping `max_heap_size`
149+ # on sandboxed processes for an otherwise modest output. A 16 MB result
150+ # comfortably fits a 128 MB cap; the old code blew past 1 GB.
151+ test "string.rep allocates proportionally to the result, not the count" do
152+ size = 16 * 1024 * 1024
153+
154+ assert { :ok , result } =
155+ heap_capped ( 128 * 1024 * 1024 , fn ->
156+ { [ string ] , _lua } = Lua . eval! ( Lua . new ( ) , ~s[ return string.rep("x", #{ size } )] )
157+ byte_size ( string )
158+ end )
159+
160+ assert result == size
161+ end
162+ end
163+
164+ # Runs `fun` in a process whose heap is capped at `byte_limit` (killed if it
165+ # exceeds), returning `{:ok, result}` or `:killed`. Lets a test assert that
166+ # work stays within a heap budget rather than sampling noisy global memory.
167+ defp heap_capped ( byte_limit , fun ) do
168+ parent = self ( )
169+ word_limit = div ( byte_limit , :erlang . system_info ( :wordsize ) )
170+ cap = % { size: word_limit , kill: true , error_logger: false }
171+
172+ { pid , ref } =
173+ :erlang . spawn_opt (
174+ fn -> send ( parent , { :result , fun . ( ) } ) end ,
175+ [ :monitor , max_heap_size: cap ]
176+ )
177+
178+ receive do
179+ { :result , result } -> { :ok , result }
180+ { :DOWN , ^ ref , :process , ^ pid , :killed } -> :killed
181+ { :DOWN , ^ ref , :process , ^ pid , reason } -> { :error , reason }
182+ after
183+ 30_000 -> :timeout
184+ end
144185 end
145186
146187 describe "string.reverse" do
0 commit comments