/*
 *  Memory allocation handling.
 */

#include "duk_internal.h"

/*
 *  Allocate memory with garbage collection.
 */

/* Slow path: voluntary GC triggered, first alloc attempt failed, or zero size. */
DUK_LOCAL DUK_NOINLINE_PERF DUK_COLD void *duk__heap_mem_alloc_slowpath(duk_heap *heap, duk_size_t size) {
	void *res;
	duk_small_int_t i;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->alloc_func != NULL);
	DUK_ASSERT_DISABLE(size >= 0);

	if (size == 0) {
		DUK_D(DUK_DPRINT("zero size alloc in slow path, return NULL"));
		return NULL;
	}

	DUK_D(DUK_DPRINT("first alloc attempt failed or voluntary GC limit reached, attempt to gc and retry"));

#if 0
	/*
	 *  If GC is already running there is no point in attempting a GC
	 *  because it will be skipped.  This could be checked for explicitly,
	 *  but it isn't actually needed: the loop below will eventually
	 *  fail resulting in a NULL.
	 */

	if (heap->ms_prevent_count != 0) {
		DUK_D(DUK_DPRINT("duk_heap_mem_alloc() failed, gc in progress (gc skipped), alloc size %ld", (long) size));
		return NULL;
	}
#endif

	/*
	 *  Retry with several GC attempts.  Initial attempts are made without
	 *  emergency mode; later attempts use emergency mode which minimizes
	 *  memory allocations forcibly.
	 */

	for (i = 0; i < DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_LIMIT; i++) {
		duk_small_uint_t flags;

		flags = 0;
		if (i >= DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_EMERGENCY_LIMIT - 1) {
			flags |= DUK_MS_FLAG_EMERGENCY;
		}

		duk_heap_mark_and_sweep(heap, flags);

		DUK_ASSERT(size > 0);
		res = heap->alloc_func(heap->heap_udata, size);
		if (res != NULL) {
			DUK_D(DUK_DPRINT("duk_heap_mem_alloc() succeeded after gc (pass %ld), alloc size %ld",
			                 (long) (i + 1), (long) size));
			return res;
		}
	}

	DUK_D(DUK_DPRINT("duk_heap_mem_alloc() failed even after gc, alloc size %ld", (long) size));
	return NULL;
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_alloc(duk_heap *heap, duk_size_t size) {
	void *res;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->alloc_func != NULL);
	DUK_ASSERT_DISABLE(size >= 0);

#if defined(DUK_USE_VOLUNTARY_GC)
	/* Voluntary periodic GC (if enabled). */
	if (DUK_UNLIKELY(--(heap)->ms_trigger_counter < 0)) {
		goto slowpath;
	}
#endif

#if defined(DUK_USE_GC_TORTURE)
	/* Simulate alloc failure on every alloc, except when mark-and-sweep
	 * is running.
	 */
	if (heap->ms_prevent_count == 0) {
		DUK_DDD(DUK_DDDPRINT("gc torture enabled, pretend that first alloc attempt fails"));
		res = NULL;
		DUK_UNREF(res);
		goto slowpath;
	}
#endif

	/* Zero-size allocation should happen very rarely (if at all), so
	 * don't check zero size on NULL; handle it in the slow path
	 * instead.  This reduces size of inlined code.
	 */
	res = heap->alloc_func(heap->heap_udata, size);
	if (DUK_LIKELY(res != NULL)) {
		return res;
	}

 slowpath:

	if (size == 0) {
		DUK_D(DUK_DPRINT("first alloc attempt returned NULL for zero size alloc, use slow path to deal with it"));
	} else {
		DUK_D(DUK_DPRINT("first alloc attempt failed, attempt to gc and retry"));
	}
	return duk__heap_mem_alloc_slowpath(heap, size);
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_alloc_zeroed(duk_heap *heap, duk_size_t size) {
	void *res;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->alloc_func != NULL);
	DUK_ASSERT_DISABLE(size >= 0);

	res = DUK_ALLOC(heap, size);
	if (DUK_LIKELY(res != NULL)) {
		duk_memzero(res, size);
	}
	return res;
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_alloc_checked(duk_hthread *thr, duk_size_t size) {
	void *res;

	DUK_ASSERT(thr != NULL);
	DUK_ASSERT(thr->heap != NULL);
	DUK_ASSERT(thr->heap->alloc_func != NULL);

	res = duk_heap_mem_alloc(thr->heap, size);
	if (DUK_LIKELY(res != NULL)) {
		return res;
	} else if (size == 0) {
		DUK_ASSERT(res == NULL);
		return res;
	}
	DUK_ERROR_ALLOC_FAILED(thr);
	DUK_WO_NORETURN(return NULL;);
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_alloc_checked_zeroed(duk_hthread *thr, duk_size_t size) {
	void *res;

	DUK_ASSERT(thr != NULL);
	DUK_ASSERT(thr->heap != NULL);
	DUK_ASSERT(thr->heap->alloc_func != NULL);

	res = duk_heap_mem_alloc(thr->heap, size);
	if (DUK_LIKELY(res != NULL)) {
		duk_memzero(res, size);
		return res;
	} else if (size == 0) {
		DUK_ASSERT(res == NULL);
		return res;
	}
	DUK_ERROR_ALLOC_FAILED(thr);
	DUK_WO_NORETURN(return NULL;);
}

/*
 *  Reallocate memory with garbage collection.
 */

/* Slow path: voluntary GC triggered, first realloc attempt failed, or zero size. */
DUK_LOCAL DUK_NOINLINE_PERF DUK_COLD void *duk__heap_mem_realloc_slowpath(duk_heap *heap, void *ptr, duk_size_t newsize) {
	void *res;
	duk_small_int_t i;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->realloc_func != NULL);
	/* ptr may be NULL */
	DUK_ASSERT_DISABLE(newsize >= 0);

	/* Newsize was 0 and realloc() returned NULL, this has the semantics
	 * of free(oldptr), i.e. memory was successfully freed.
	 */
	if (newsize == 0) {
		DUK_D(DUK_DPRINT("zero size realloc in slow path, return NULL"));
		return NULL;
	}

	DUK_D(DUK_DPRINT("first realloc attempt failed, attempt to gc and retry"));

#if 0
	/*
	 *  Avoid a GC if GC is already running.  See duk_heap_mem_alloc().
	 */

	if (heap->ms_prevent_count != 0) {
		DUK_D(DUK_DPRINT("duk_heap_mem_realloc() failed, gc in progress (gc skipped), alloc size %ld", (long) newsize));
		return NULL;
	}
#endif

	/*
	 *  Retry with several GC attempts.  Initial attempts are made without
	 *  emergency mode; later attempts use emergency mode which minimizes
	 *  memory allocations forcibly.
	 */

	for (i = 0; i < DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_LIMIT; i++) {
		duk_small_uint_t flags;

		flags = 0;
		if (i >= DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_EMERGENCY_LIMIT - 1) {
			flags |= DUK_MS_FLAG_EMERGENCY;
		}

		duk_heap_mark_and_sweep(heap, flags);

		DUK_ASSERT(newsize > 0);
		res = heap->realloc_func(heap->heap_udata, ptr, newsize);
		if (res || newsize == 0) {
			DUK_D(DUK_DPRINT("duk_heap_mem_realloc() succeeded after gc (pass %ld), alloc size %ld",
			                 (long) (i + 1), (long) newsize));
			return res;
		}
	}

	DUK_D(DUK_DPRINT("duk_heap_mem_realloc() failed even after gc, alloc size %ld", (long) newsize));
	return NULL;
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_realloc(duk_heap *heap, void *ptr, duk_size_t newsize) {
	void *res;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->realloc_func != NULL);
	/* ptr may be NULL */
	DUK_ASSERT_DISABLE(newsize >= 0);

#if defined(DUK_USE_VOLUNTARY_GC)
	/* Voluntary periodic GC (if enabled). */
	if (DUK_UNLIKELY(--(heap)->ms_trigger_counter < 0)) {
		goto slowpath;
	}
#endif

#if defined(DUK_USE_GC_TORTURE)
	/* Simulate alloc failure on every realloc, except when mark-and-sweep
	 * is running.
	 */
	if (heap->ms_prevent_count == 0) {
		DUK_DDD(DUK_DDDPRINT("gc torture enabled, pretend that first realloc attempt fails"));
		res = NULL;
		DUK_UNREF(res);
		goto slowpath;
	}
#endif

	res = heap->realloc_func(heap->heap_udata, ptr, newsize);
	if (DUK_LIKELY(res != NULL)) {
		return res;
	}

 slowpath:

	if (newsize == 0) {
		DUK_D(DUK_DPRINT("first realloc attempt returned NULL for zero size realloc, use slow path to deal with it"));
	} else {
		DUK_D(DUK_DPRINT("first realloc attempt failed, attempt to gc and retry"));
	}
	return duk__heap_mem_realloc_slowpath(heap, ptr, newsize);
}

/*
 *  Reallocate memory with garbage collection, using a callback to provide
 *  the current allocated pointer.  This variant is used when a mark-and-sweep
 *  (e.g. finalizers) might change the original pointer.
 */

/* Slow path: voluntary GC triggered, first realloc attempt failed, or zero size. */
DUK_LOCAL DUK_NOINLINE_PERF DUK_COLD void *duk__heap_mem_realloc_indirect_slowpath(duk_heap *heap, duk_mem_getptr cb, void *ud, duk_size_t newsize) {
	void *res;
	duk_small_int_t i;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->realloc_func != NULL);
	DUK_ASSERT_DISABLE(newsize >= 0);

	if (newsize == 0) {
		DUK_D(DUK_DPRINT("zero size indirect realloc in slow path, return NULL"));
		return NULL;
	}

	DUK_D(DUK_DPRINT("first indirect realloc attempt failed, attempt to gc and retry"));

#if 0
	/*
	 *  Avoid a GC if GC is already running.  See duk_heap_mem_alloc().
	 */

	if (heap->ms_prevent_count != 0) {
		DUK_D(DUK_DPRINT("duk_heap_mem_realloc_indirect() failed, gc in progress (gc skipped), alloc size %ld", (long) newsize));
		return NULL;
	}
#endif

	/*
	 *  Retry with several GC attempts.  Initial attempts are made without
	 *  emergency mode; later attempts use emergency mode which minimizes
	 *  memory allocations forcibly.
	 */

	for (i = 0; i < DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_LIMIT; i++) {
		duk_small_uint_t flags;

#if defined(DUK_USE_DEBUG)
		void *ptr_pre;
		void *ptr_post;
#endif

#if defined(DUK_USE_DEBUG)
		ptr_pre = cb(heap, ud);
#endif
		flags = 0;
		if (i >= DUK_HEAP_ALLOC_FAIL_MARKANDSWEEP_EMERGENCY_LIMIT - 1) {
			flags |= DUK_MS_FLAG_EMERGENCY;
		}

		duk_heap_mark_and_sweep(heap, flags);
#if defined(DUK_USE_DEBUG)
		ptr_post = cb(heap, ud);
		if (ptr_pre != ptr_post) {
			DUK_DD(DUK_DDPRINT("realloc base pointer changed by mark-and-sweep: %p -> %p",
			                   (void *) ptr_pre, (void *) ptr_post));
		}
#endif

		/* Note: key issue here is to re-lookup the base pointer on every attempt.
		 * The pointer being reallocated may change after every mark-and-sweep.
		 */

		DUK_ASSERT(newsize > 0);
		res = heap->realloc_func(heap->heap_udata, cb(heap, ud), newsize);
		if (res || newsize == 0) {
			DUK_D(DUK_DPRINT("duk_heap_mem_realloc_indirect() succeeded after gc (pass %ld), alloc size %ld",
			                 (long) (i + 1), (long) newsize));
			return res;
		}
	}

	DUK_D(DUK_DPRINT("duk_heap_mem_realloc_indirect() failed even after gc, alloc size %ld", (long) newsize));
	return NULL;
}

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void *duk_heap_mem_realloc_indirect(duk_heap *heap, duk_mem_getptr cb, void *ud, duk_size_t newsize) {
	void *res;

	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->realloc_func != NULL);
	DUK_ASSERT_DISABLE(newsize >= 0);

#if defined(DUK_USE_VOLUNTARY_GC)
	/* Voluntary periodic GC (if enabled). */
	if (DUK_UNLIKELY(--(heap)->ms_trigger_counter < 0)) {
		goto slowpath;
	}
#endif

#if defined(DUK_USE_GC_TORTURE)
	/* Simulate alloc failure on every realloc, except when mark-and-sweep
	 * is running.
	 */
	if (heap->ms_prevent_count == 0) {
		DUK_DDD(DUK_DDDPRINT("gc torture enabled, pretend that first indirect realloc attempt fails"));
		res = NULL;
		DUK_UNREF(res);
		goto slowpath;
	}
#endif

	res = heap->realloc_func(heap->heap_udata, cb(heap, ud), newsize);
	if (DUK_LIKELY(res != NULL)) {
		return res;
	}

 slowpath:

	if (newsize == 0) {
		DUK_D(DUK_DPRINT("first indirect realloc attempt returned NULL for zero size realloc, use slow path to deal with it"));
	} else {
		DUK_D(DUK_DPRINT("first indirect realloc attempt failed, attempt to gc and retry"));
	}
	return duk__heap_mem_realloc_indirect_slowpath(heap, cb, ud, newsize);
}

/*
 *  Free memory
 */

DUK_INTERNAL DUK_INLINE_PERF DUK_HOT void duk_heap_mem_free(duk_heap *heap, void *ptr) {
	DUK_ASSERT(heap != NULL);
	DUK_ASSERT(heap->free_func != NULL);
	/* ptr may be NULL */

	/* Must behave like a no-op with NULL and any pointer returned from
	 * malloc/realloc with zero size.
	 */
	heap->free_func(heap->heap_udata, ptr);

	/* Never perform a GC (even voluntary) in a memory free, otherwise
	 * all call sites doing frees would need to deal with the side effects.
	 * No need to update voluntary GC counter either.
	 */
}
