CARGO ?= $(HOME)/.cargo/bin/cargo
SBCL ?= sbcl

.PHONY: build test-m1 test-m2 test-m3 test-m4 test-fetch test-fetch-ccl test-ccl test-ecl-program audit doc check-versions compat dist-dryrun check-1.0 check-dist bench clean clean-cache

build:
	$(CARGO) build

test-m1:
	$(SBCL) --non-interactive --load tests/run-m1.lisp

test-m2:
	$(SBCL) --non-interactive --load tests/run-m2.lisp

test-m3:
	$(CARGO) test --workspace
	$(SBCL) --non-interactive --load tests/run-m3.lisp

test-m4:
	$(CARGO) test --workspace
	$(SBCL) --non-interactive --load tests/run-m4.lisp

test-fetch:
	sh examples/fetch/audit.sh
	$(SBCL) --non-interactive --load tests/run-fetch.lisp

# the same suite on Clozure CL (the dump tests use the CCL harness from m7)
test-fetch-ccl:
	sh examples/fetch/audit.sh
	$(CCL) --batch --load tests/run-fetch.lisp

# examples/httpd: the HTTP server with Lisp handlers (fetch's route — its
# own runner, not run-m4; hermetic, the suite talks to 127.0.0.1 only)
test-httpd:
	sh examples/httpd/audit.sh
	$(SBCL) --non-interactive --load tests/run-httpd.lisp

test-httpd-ccl:
	sh examples/httpd/audit.sh
	$(CCL) --batch --load tests/run-httpd.lisp

# ECL has no image dump: applications ship as asdf:program-op executables.
# Builds the minimal consumer in tests/ecl-program and runs it against the
# wordbag artifact. ECL exits 0 even from its debugger, so the marker is
# the gate, not the exit code — except a timeout kill (124), which must
# fail on its own, so the run is not piped (a pipe would discard it).
ECL ?= ecl
test-ecl-program:
	$(CARGO) build -p wordbag
	rm -f tests/ecl-program/rulisp-ecl-smoke
	$(ECL) --norc --load tests/ecl-program/build.lisp > ecl-program-build.log 2>&1; \
	  grep -q BUILD-DONE ecl-program-build.log || { tail -40 ecl-program-build.log; exit 1; }
	RULISP_SMOKE_CRATE=$(CURDIR)/target/debug/libwordbag.so timeout 120 \
	  tests/ecl-program/rulisp-ecl-smoke </dev/null > ecl-program-run.log 2>&1; \
	  st=$$?; cat ecl-program-run.log; test $$st -eq 0 \
	  && grep -q ECL-PROGRAM-OK ecl-program-run.log && ! grep -q FAIL ecl-program-run.log

# BOUNDARY §7 as a gate over every example, with a self-test proving the
# audit can still fail (tools/audit-fixture imports signal()).
audit:
	sh tools/rulisp-audit-selftest.sh
	$(CARGO) build --workspace
	for c in wordbag rx wasm fetch httpd; do \
	  f=$$(ls target/debug/lib$$c.so target/debug/lib$$c.dylib target/debug/$$c.dll 2>/dev/null | head -1); \
	  sh tools/rulisp-audit.sh "$$f" examples/$$c || exit 1; done

# what docs.rs will show: no missing docs, no broken links, doctest compiles
doc:
	RUSTDOCFLAGS="-D warnings" $(CARGO) doc --no-deps -p rulisp -p rulisp-macros -p rulisp-runtime
	$(CARGO) test --doc -p rulisp

# docs/stability.md §8 criterion 3, checked (Linux): the previous release's
# loader loads the tree's crate (forward); every export the previous
# release had is still in this tree's Lisp API golden with the same kind,
# superclasses and lambda list (tests/compat/api-subset.lisp — additions
# pass, a removal or a changed signature fails by name); and the previous
# release's test suite runs against the tree's loader (its calls, our
# code) — through an assembled rulisp.asd: this tree's `rulisp` defsystem
# (so a loader file this tree added loads) plus the previous release's
# `rulisp/test`. PREV is the last release; docs/releasing.md step 9 moves
# it. When the old suite fails: a test that reaches rulisp:: internals is
# recorded in tests/compat/skipped-internal.txt, with its reason, and not
# counted; a failure through an exported symbol is a break and does not
# land (tests/compat/old-suite-verdict.sh decides, by name).
PREV ?= v0.8.0
COMPAT := $(CURDIR)/target/compat
compat:
	$(CARGO) build -p wordbag
	rm -rf $(COMPAT) && mkdir -p $(COMPAT)/loader $(COMPAT)/tree
	git archive $(PREV) lisp | tar -x -C $(COMPAT)/loader
	RULISP_PREV=$(PREV) RULISP_PREV_LISP=$(COMPAT)/loader/lisp/ \
	  RULISP_ARTIFACT=$(CURDIR)/target/debug/libwordbag.so \
	  $(SBCL) --non-interactive --load tests/compat/old-loader.lisp 2>&1 | tee $(COMPAT)/old-loader.log
	grep -q "OLD-LOADER-OK Hello, $(PREV) loader!" $(COMPAT)/old-loader.log
	git archive $(PREV) | tar -x -C $(COMPAT)/tree
	cp lisp/src/*.lisp $(COMPAT)/tree/lisp/src/
	mv $(COMPAT)/tree/lisp/rulisp.asd $(COMPAT)/tree/lisp/rulisp.asd.orig
	awk '/^\(asdf:defsystem #:rulisp\/test/{exit} {print}' lisp/rulisp.asd > $(COMPAT)/tree/lisp/rulisp.asd
	awk '/^\(asdf:defsystem #:rulisp\/test/,0' $(COMPAT)/tree/lisp/rulisp.asd.orig >> $(COMPAT)/tree/lisp/rulisp.asd
	test "$$(grep -c '^(asdf:defsystem #:rulisp' $(COMPAT)/tree/lisp/rulisp.asd)" -eq 2
	cp $(COMPAT)/tree/tests/golden/lisp-api.sexp $(COMPAT)/prev-lisp-api.sexp
	cp tests/golden/lisp-api.sexp $(COMPAT)/tree/tests/golden/lisp-api.sexp
	RULISP_PREV_API=$(COMPAT)/prev-lisp-api.sexp RULISP_API=$(CURDIR)/tests/golden/lisp-api.sexp \
	  $(SBCL) --non-interactive --load tests/compat/api-subset.lisp 2>&1 | tee $(COMPAT)/api-subset.log
	grep -q API-SUBSET-OK $(COMPAT)/api-subset.log
	cd $(COMPAT)/tree && $(SBCL) --non-interactive --load tests/run-m4.lisp > $(COMPAT)/old-suite.log 2>&1; \
	  tail -4 $(COMPAT)/old-suite.log
	sh tests/compat/old-suite-verdict.sh $(COMPAT)/old-suite.log tests/compat/skipped-internal.txt

# docs/stability.md §9: what the Quicklisp dist builder would do with a
# source tarball of HEAD — every system of every .asd loads with no cargo
# reachable: off PATH, and RULISP_CARGO pointed at nothing, since rulisp's
# lookup would otherwise find ~/.cargo/bin/cargo (tests/dist-dryrun.lisp
# prints DRYRUN-OK per system; two today: rulisp and rulisp/test — the ECL
# smoke consumer's system file is a template its build script copies, so
# the tarball has one .asd).
DIST := $(CURDIR)/target/dist
dist-dryrun:
	rm -rf $(DIST) && mkdir -p $(DIST)
	git archive HEAD | tar -x -C $(DIST)
	env -i HOME="$$HOME" PATH=/bin:/usr/bin RULISP_CARGO=/nonexistent/cargo RULISP_DIST_ROOT=$(DIST) \
	  sbcl --non-interactive --load $(CURDIR)/tests/dist-dryrun.lisp 2>&1 | tee $(DIST).log
	test "$$(grep -c '^DRYRUN-OK' $(DIST).log)" -eq 2

# docs/stability.md §8: the 1.0 exit criteria as commands (constants,
# gate names, counts); run by the MSRV CI job next to check-versions
check-1.0:
	sh tools/check-1.0.sh

# docs/releasing.md step 8: which rulisp the Ultralisp dist serves today
check-dist:
	sh tools/check-dist.sh

# one version string across the crates, the path pins, the ASDF system
# and the docs (docs/releasing.md step 1); fails on any site that disagrees
check-versions:
	sh tools/check-versions.sh

bench:
	$(SBCL) --non-interactive --load tests/bench.lisp

# best-effort second implementation (download CCL, then: make test-ccl)
CCL ?= $(HOME)/ccl/lx86cl64
test-ccl:
	$(CCL) --batch --load tests/run-m4.lisp

# Every regenerable artifact the targets above leave in the tree — exactly
# what .gitignore hides, by name (not `git clean -X`, which would also take
# a developer's own ignored files):
#   target/                         cargo; also make compat, dist-dryrun, audit
#   examples/*/target               use-crate builds each crate into its own
#                                   directory (test-m4, test-fetch, test-ccl)
#   */target-abort-check            m1's panic=abort probe
#   tests/m1-handwritten/target     the ABI oracle (test-m2)
#   tests/abi-fixture/target        v06.abi-mismatch-refused
#   tests/inbox-fixture/target      the v09 suite (rulisp::Inbox)
#   tests/wasm-guests/rust-guest/target   only if you rebuilt the Rust guest
#   tools/audit-fixture/target      the audit self-test
#   tests/ecl-program/…             the program-op executable, its generated
#                                   .asd and the two logs (test-ecl-program)
# Nothing under $$HOME is touched: the loader's cache is `make clean-cache`.
clean:
	$(CARGO) clean
	rm -rf examples/*/target examples/*/target-abort-check \
	       tests/m1-handwritten/target tests/m1-handwritten/target-abort-check \
	       tests/abi-fixture/target tests/inbox-fixture/target tools/audit-fixture/target \
	       tests/wasm-guests/rust-guest/target \
	       tests/ecl-program/rulisp-ecl-smoke tests/ecl-program/rulisp-ecl-smoke.asd \
	       ecl-program-*.log

# The loader's cache of library copies (docs/installation.md "Library
# cache") lives outside the tree and is shared with other checkouts and
# with running images, which sweep it themselves — so it is removed only on
# request. Prints its size first.
clean-cache:
	$(SBCL) --non-interactive --eval '(require :asdf)' \
	  --eval '(let ((d (uiop:xdg-cache-home "rulisp/"))) (format t "~&~A: ~A~%" d (if (probe-file d) (string-trim (list #\Newline) (uiop:run-program (list "du" "-sh" (namestring d)) :output :string)) "absent")) (uiop:delete-directory-tree d :validate (lambda (p) (search "rulisp" (namestring p))) :if-does-not-exist :ignore))'
