# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

# src/test/basic -- host-side BASIC end-to-end tests (suite T7)
#
#   make test            headless sample/fixture runs, compaction
#                        differential, compiler fuzz, bytecode fuzz
#   make test TESTSAN=1  same with the fuzzers built under ASan/UBSan
#                        (bin/host/basrun and bascomp come from the
#                        dvxbasic Makefile, which honours TESTSAN too)
#   make fuzz COV=1      fuzzers built with gcov instrumentation under
#                        obj/hostcov (same split as the dvxbasic Makefile)
#
#   make bench           build basBench (-DBAS_BENCH, allocators wrapped)
#                        and run every bench/*.bas plus the compile
#                        benchmarks; table in obj/test/bench/results.txt
#   make bench-compare   per-benchmark change from BENCH_OLD (default
#                        bench/baseline.txt, the Phase 0 baseline) to
#                        BENCH_NEW (default the last make bench)
#
# Tunables: FUZZ_N (mutations per seed), FUZZ_SEED, FUZZ_STEPS, FUZZ_MEM_MB;
# BENCH_ARGS (extra basBench options, e.g. -runs 9, -profile, -filter NAME).

include ../../common.mk

DVXBASIC    = ../../apps/kpunch/dvxbasic
LIBDVX      = ../../libs/kpunch/libdvx
INCLUDES    = -I$(DVXBASIC) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(LIBDVX)/../texthelp
HOSTCFLAGS  = -O2 -g $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DEPFLAGS) $(INCLUDES)
HOSTLDFLAGS =
ifeq ($(TESTSAN),1)
HOSTCFLAGS  += $(SAN_FLAGS)
HOSTLDFLAGS += $(SAN_FLAGS)
endif

REPO        = ../../..
# COV=1: instrumented binaries and their scratch tree live under
# obj/hostcov so they never mix with the sanitized or release builds.
ifeq ($(COV),1)
HOSTCFLAGS  += --coverage
HOSTLDFLAGS += --coverage
HOSTDIR      = $(REPO)/obj/hostcov/basic
OUTDIR       = $(REPO)/obj/hostcov/test/basic
else
HOSTDIR      = $(REPO)/bin/host
OUTDIR       = $(REPO)/obj/test/basic
endif
# Fuzzer objects, one directory per TESTSAN mode.
FUZZ_OBJDIR = $(OUTDIR)/obj-$(if $(filter 1,$(TESTSAN)),san,plain)
CORPUS      = $(abspath $(OUTDIR)/corpus)
FUZZWORK    = $(OUTDIR)/fuzzwork
GOLDEN      = $(abspath $(REPO)/src/test/samples/golden)

FUZZ_N      = 10
FUZZ_SEED   = 12345
FUZZ_STEPS  = 200000
FUZZ_MEM_MB = 512

# Corrupt length fields legitimately ask for huge allocations; the
# deserializer must handle a NULL from malloc, so let ASan return one
# instead of aborting the run.  A fuzzer gets FUZZ_MEM_MB of memory, as
# a DOS machine has a limit: past it an allocation fails (a program gets
# Out of memory) rather than the host running out.  ASan builds use its
# soft RSS limit (an address-space limit breaks its shadow memory),
# plain builds an address-space limit.
ASAN_ENV    = ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1:soft_rss_limit_mb=$(FUZZ_MEM_MB)
ifeq ($(TESTSAN),1)
FUZZ_MEM_CAP = true
else
FUZZ_MEM_CAP = ulimit -v $$(($(FUZZ_MEM_MB) * 1024))
endif

COMPILER_SRCS = $(DVXBASIC)/compiler/lexer.c $(DVXBASIC)/compiler/ast.c $(DVXBASIC)/compiler/scope.c $(DVXBASIC)/compiler/parse.c $(DVXBASIC)/compiler/parseExpr.c $(DVXBASIC)/compiler/parseStmt.c $(DVXBASIC)/compiler/parseDecl.c $(DVXBASIC)/compiler/resolve.c $(DVXBASIC)/compiler/resolveExpr.c $(DVXBASIC)/compiler/typecheck.c $(DVXBASIC)/compiler/fold.c $(DVXBASIC)/compiler/gen.c $(DVXBASIC)/compiler/genExpr.c $(DVXBASIC)/compiler/genStmt.c $(DVXBASIC)/compiler/compiler.c $(DVXBASIC)/compiler/codegen.c
RUNTIME_SRCS  = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/dates.c $(DVXBASIC)/runtime/serialize.c $(DVXBASIC)/runtime/gfx.c $(DVXBASIC)/runtime/screen.c $(LIBDVX)/dvxPrefs.c $(LIBDVX)/dvxFontFmt.c
SUPPORT_SRCS  = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c

FUZZ_COMPILE_SRCS = fuzzCompile.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(SUPPORT_SRCS)
FUZZ_MODULE_SRCS  = fuzzModule.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(LIBDVX)/dvxResource.c ../../tools/dvxResWrite.c $(SUPPORT_SRCS)

# fuzzObjs SRCS -> fuzzer objects for SRCS
fuzzObjs = $(patsubst %.c,$(FUZZ_OBJDIR)/%.o,$(notdir $1))
FUZZ_COMPILE_OBJS = $(call fuzzObjs,$(FUZZ_COMPILE_SRCS))
FUZZ_MODULE_OBJS  = $(call fuzzObjs,$(FUZZ_MODULE_SRCS))

FUZZ_COMPILE = $(abspath $(HOSTDIR)/fuzzCompile)
FUZZ_MODULE  = $(abspath $(HOSTDIR)/fuzzModule)

HARNESS_SRCS = $(DVXBASIC)/test_suite.c $(DVXBASIC)/test_compiler.c $(DVXBASIC)/test_compact.c

TESTSAN_STAMP = $(OUTDIR)/.testsan-$(if $(filter 1,$(TESTSAN)),on,off)

.PHONY: all build test corpus runs fuzz clean bench bench-build bench-compare

all: build

build: $(FUZZ_COMPILE) $(FUZZ_MODULE)

# bench-build too, so the benchmark cannot stop compiling unnoticed
test: runs fuzz bench-build

# basrun-driven sample / fixture / compaction runs
runs:
	./runBasic.sh

# Regenerate the seed corpus from the C harnesses every time: cheap,
# and it can never go stale.
corpus: | $(OUTDIR)
	rm -rf $(CORPUS)
	./extractCorpus.py $(CORPUS) $(HARNESS_SRCS)

# Fuzzers run inside a scratch directory so file-system statements in
# mutated programs only ever touch files there.
fuzz: build corpus
	rm -rf $(FUZZWORK)
	mkdir -p $(FUZZWORK)
	cd $(FUZZWORK) && $(FUZZ_MEM_CAP) && $(ASAN_ENV) $(FUZZ_COMPILE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) $(CORPUS)
	cd $(FUZZWORK) && $(FUZZ_MEM_CAP) && $(ASAN_ENV) $(FUZZ_MODULE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) -corpus $(CORPUS) $(wildcard $(GOLDEN)/*.app)

$(FUZZ_COMPILE): $(FUZZ_COMPILE_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
	$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(FUZZ_COMPILE_OBJS) -lm

$(FUZZ_MODULE): $(FUZZ_MODULE_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
	$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(FUZZ_MODULE_OBJS) -lm

define FUZZ_OBJ_RULE
$(call fuzzObjs,$1): $1 $$(DVX_MAKEFILES) | $$(FUZZ_OBJDIR)
	$$(HOSTCC) $$(HOSTCFLAGS) -c -o $$@ $$<
endef

$(foreach src,$(sort $(FUZZ_COMPILE_SRCS) $(FUZZ_MODULE_SRCS)),$(eval $(call FUZZ_OBJ_RULE,$(src))))

$(TESTSAN_STAMP): | $(OUTDIR)
	rm -f $(OUTDIR)/.testsan-on $(OUTDIR)/.testsan-off
	touch $@

$(HOSTDIR) $(OUTDIR) $(FUZZ_OBJDIR):
	mkdir -p $@

# ---- Benchmarks ----
#
# Its own object tree: the VM is compiled with -DBAS_BENCH (per-opcode
# instruction counts) and the link wraps the allocators, so none of it
# reaches any other build.  Always optimized and unsanitized (timing).
BENCH_OUTDIR  = $(REPO)/obj/test/bench
BENCH_OBJDIR  = $(BENCH_OUTDIR)/obj
BENCH_WORK    = $(BENCH_OUTDIR)/work
BENCH         = $(BENCH_OUTDIR)/basBench
BENCH_RESULTS = $(BENCH_OUTDIR)/results.txt
BENCH_ARGS    =
BENCH_OLD     = bench/baseline.txt
BENCH_NEW     = $(BENCH_RESULTS)
BENCH_CFLAGS  = $(HOST_TOOL_CFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE -DBAS_BENCH $(INCLUDES)
BENCH_LDFLAGS = -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup
BENCH_SRCS    = basBench.c $(COMPILER_SRCS) $(DVXBASIC)/compiler/strip.c $(DVXBASIC)/compiler/obfuscate.c $(DVXBASIC)/compiler/compact.c $(DVXBASIC)/basBuild.c $(RUNTIME_SRCS) $(LIBDVX)/dvxResource.c ../../tools/dvxResWrite.c $(SUPPORT_SRCS)
BENCH_OBJS    = $(patsubst %.c,$(BENCH_OBJDIR)/%.o,$(notdir $(BENCH_SRCS)))
BENCH_PROGS   = $(sort $(wildcard bench/*.bas))
BENCH_SAMPLES = $(sort $(shell find $(DVXBASIC)/.. -name "*.dbp"))

bench-build: $(BENCH)

bench: $(BENCH)
	rm -rf $(BENCH_WORK)
	$(BENCH) -o $(abspath $(BENCH_RESULTS)) -work $(abspath $(BENCH_WORK)) -opcodes $(abspath $(DVXBASIC)/compiler/opcodes.h) $(BENCH_ARGS) $(BENCH_PROGS) $(BENCH_SAMPLES)

bench-compare: $(BENCH)
	$(BENCH) -compare $(BENCH_OLD) $(BENCH_NEW)

$(BENCH): $(BENCH_OBJS) | $(BENCH_OUTDIR)
	$(HOSTCC) $(BENCH_LDFLAGS) -o $@ $(BENCH_OBJS) -lm

define BENCH_OBJ_RULE
$(patsubst %.c,$(BENCH_OBJDIR)/%.o,$(notdir $1)): $1 $$(DVX_MAKEFILES) | $$(BENCH_OBJDIR)
	$$(HOSTCC) $$(BENCH_CFLAGS) -c -o $$@ $$<
endef

$(foreach src,$(BENCH_SRCS),$(eval $(call BENCH_OBJ_RULE,$(src))))

$(BENCH_OUTDIR) $(BENCH_OBJDIR):
	mkdir -p $@

-include $(patsubst %.o,%.d,$(sort $(FUZZ_COMPILE_OBJS) $(FUZZ_MODULE_OBJS) $(BENCH_OBJS)))

clean:
	rm -rf $(OUTDIR) $(FUZZ_COMPILE) $(FUZZ_MODULE) $(BENCH_OUTDIR)
