# Build the interpreter and extract executable documentation into build/.
# make test runs the language, CLI, and runtime regressions; make bench times
# the benchmark workloads. See README.md for TypeScript and Babel commands.

# tsc compiles the interpreter and the test suites into build/. Babel can
# instead, without checking types: make BABEL=babeljs test.
ifdef BABEL
BABEL_PLUGINS = @babel/plugin-transform-typescript,@babel/plugin-transform-modules-commonjs
TSC = $(BABEL) interpreter --extensions .ts --plugins $(BABEL_PLUGINS) --out-dir build
TEST_TSC = $(BABEL) tests --extensions .ts --ignore '**/*.d.ts' --plugins $(BABEL_PLUGINS) \
    --out-dir build
else
TSC = tsc --project interpreter/tsconfig.json
TEST_TSC = tsc --project tests/tsconfig.json
endif
RUN = node build/5emel.js
BENCH_ARGS =
VIM_COMMAND ?= vim
INTERPRETER = build/5emel.js build/memo.js build/simulation.js build/main.js
SUITES = build/reflection-tests.js build/syntax-tests.js build/repl-tests.js \
    build/os-tests.js build/canonical-tests.js build/interfaces-tests.js \
    build/interrupt-tests.js build/terminal-tests.js build/simulation-tests.js \
    build/memo-simulation-tests.js build/driver-tests.js build/build-tests.js \
    build/compiler-tests.js

test: $(INTERPRETER) build/examples.5em build/lines.5em $(SUITES) tests/tests.5em \
    benchmarks/bench.5em build/notebook.html
	awk -f tools/width.awk interpreter/*.ts tests/*.ts benchmarks/*.js notebook/*.js \
	    compiler/*.5em runtime/*.c \
	    tools/notebook.js README.md docs/*.md
	node notebook/check.js
	$(RUN) -e 'tests()' build/examples.5em tests/tests.5em
	$(RUN) -m -e 'reflection_tests()' build/examples.5em tests/tests.5em
	node build/reflection-tests.js
	node build/syntax-tests.js
	node build/repl-tests.js
	node build/os-tests.js
	node --expose-gc build/canonical-tests.js
	node build/interfaces-tests.js
	node build/interrupt-tests.js
	node build/terminal-tests.js
	node build/terminal-tests.js --simulate
	node build/terminal-tests.js --memo-sampled
	node build/simulation-tests.js
	node build/memo-simulation-tests.js
	node build/driver-tests.js
	node build/build-tests.js
	node build/compiler-tests.js
	node build/compiler-tests.js --gc-stress
	node build/compiler-tests.js --memo-all
	node build/compiler-tests.js --memo-all --gc-stress
	$(RUN) -e 'bench_tests()' build/examples.5em benchmarks/bench.5em
	test "$$(printf 'x^3 + 2*x\n' | $(RUN) build/examples.5em)" = '((3 * x^2) + 2)'
	test "$$(printf 'a\nb\nc\n' | $(RUN) build/lines.5em)" = 'lines: 3'
	$(RUN) -e 'd(Foo, "x")' build/examples.5em 2>&1 | grep -qF 'in d(Foo, "x"), called at -e:1'
	$(RUN) -e '1 = 2 -> fib(1, 2) | 3' build/examples.5em 2>&1 | grep -q 'fib takes 1 argument'
	$(RUN) -e '((:y = 1 | :z = 2); y)' 2>&1 | grep -q 'only some paths'
	$(RUN) -e '"\x4"' 2>&1 | grep -qF '\x takes two hexadecimal digits'
	$(RUN) -e "'ab'" 2>&1 | grep -q 'a character literal is one byte'
	test "$$($(RUN) -e 'die("no")' 2>&1)" = no
	$(RUN) -e '((:y = 1) -> fail | []); y' 2>&1 | grep -q 'only some paths'
	test "$$($(RUN) -e 'loop :next(:i = 0) (| i < 3; next(i + 1) | i)')" = 3
	$(RUN) -e 'loop :next(:i = 0) (| i < 3; 1 + next(i + 1) | i)' 2>&1 \
	    | grep -q 'not in tail position'
	$(RUN) -e 'loop :next(:i = 0) (loop :next(:j = 0) (j))' 2>&1 \
	    | grep -q 'next is already bound'
	test "$$($(RUN) -e '1 -> fail | 42')" = lost
	test "$$($(RUN) -e '1; fail -> 7 | 42' 2>/dev/null)" = 42
	$(RUN) -e '(1 -> fail) | 42' 2>&1 | grep -q 'in the last alternative'
	$(RUN) -e '1 -> fail -> 7 | 42' 2>&1 | grep -q 'twice in one alternative'
	$(RUN) -e 'loop :next(:i = 0) ((i < 3 -> next(i + 1)) | i)' 2>&1 \
	    | grep -q 'in the last alternative'
	$(RUN) -e 'byte("", 0) | 99' 2>&1 | grep -qF 'byte("", 0) out of range'
	$(RUN) -e 'slice("abc", 2, 1) | ""' 2>&1 | grep -qF 'slice("abc", 2, 1) out of range'
	test "$$($(RUN) -e '[byte("abc", 2), slice("abc", 3, 3)]')" = '[99, ""]'
	$(RUN) -e '(:x = 1 | fail); x' 2>&1 | grep -q 'only some paths'
	test "$$($(RUN) -e ':x = (1 | fail); x')" = 1
	$(RUN) -e '(:x = 1 | 2); loop :next(:x = 3) (x)' 2>&1 \
	    | grep -q 'x is already bound on some paths'
	test "$$($(RUN) -e -4611686018427387904)" = -4611686018427387904
	$(RUN) -e 4611686018427387904 2>&1 | grep -q 'integer overflow'
	test "$$($(RUN) -e 1 build/examples.5em tests/tests.5em benchmarks/bench.5em 2>&1)" = 1
	test "$$($(RUN) -e '[:x, ...:r] = [1, 2, 3]; [r, ...x]')" = '[[2, 3], ...1]'
	$(RUN) -e '[...[]]' 2>&1 | grep -q 'unexpected \.\.\.'
	test "$$($(RUN) -e '[1 < 0 -> 5 | 7, show(1 | 2)]' 2>/dev/null)" = '[7, "1"]'
	$(RUN) -e '[1 | 2]' 2>&1 | grep -q "this alternative can't lose"
	$(RUN) -e ':x = 1; (x -> 2 | 3)' 2>&1 | grep -q "this guard can't lose"
	$(RUN) -e '(1 < 2 -> 3 | 4)' 2>&1 | grep -q 'nothing after -> can lose, so it means ;'
	test -z "$$($(RUN) -e '(1 < 2 -> 3 = 3 | 4)' 2>&1 >/dev/null)"
	test "$$($(RUN) -e 'flatbytes(["ab", ..."cd"])')" = '"abcd"'
	$(RUN) -e 'concat([])' 2>&1 | grep -q 'concat is not a variable or a function'
	$(MAKE) --no-print-directory test-unzip test-pngbench test-tt_bench_eval

# Reports setup, first evaluation, repeated requests, and whole-command times.
# Raw samples and call counters are written to build/bench-results.json.
bench: $(INTERPRETER) build/examples.5em benchmarks/bench.5em
	node benchmarks/run.js $(BENCH_ARGS)

unzip: build/unzip

# 5emelc reads stdin; its filename argument labels source locations.
# Utilities and generated test drivers share this compilation recipe.
define compile_5emel
	build/5emelc "$(1)" < "$(1)" > "$(2).s"
	gcc -O2 -o "$(2)" "$(2).s" runtime/rt.c
endef

# For generated fixtures: make compile-5emel SOURCE=/tmp/test.5em OUTPUT=/tmp/test
compile-5emel: build/5emelc runtime/rt.c | build
	$(if $(and $(SOURCE),$(OUTPUT)),,$(error compile-5emel requires SOURCE and OUTPUT))
	$(call compile_5emel,$(SOURCE),$(OUTPUT))

build/unzip: unzip/unzip.5em build/5emelc runtime/rt.c | build
	$(call compile_5emel,unzip/unzip.5em,$@)

test-unzip: build/unzip
	node unzip/test.js build/unzip

pngbench: build/pngbench

build/pngbench: pngbench/pngbench.5em build/5emelc runtime/rt.c | build
	$(call compile_5emel,pngbench/pngbench.5em,$@)

test-pngbench: build/pngbench build/unzip build/png-reference
	node pngbench/test.js build/pngbench

# Test oracle only: PngBench's checks decode its PNGs with libpng too.
build/png-reference: pngbench/pngdecode.c | build
	gcc -O2 -Wall -Wextra -o $@ $< -lpng

bench-pngbench: build/pngbench
	node pngbench/bench.js build/pngbench $(PNGBENCH_ARGS)

tt_bench_eval: build/tt_bench_eval

build/tt_bench_eval: tt_bench_eval/tt_bench_eval.5em build/5emelc runtime/rt.c | build
	$(call compile_5emel,tt_bench_eval/tt_bench_eval.5em,$@)

# Its checks compare with FreeType, so without FreeType's development
# files and pkg-config they are skipped, with a warning.
test-tt_bench_eval: build/tt_bench_eval build/pngbench
	@if pkg-config --exists freetype2 2>/dev/null; then \
	  $(MAKE) --no-print-directory build/tt-reference && \
	  node tt_bench_eval/test.js build/tt_bench_eval; \
	else \
	  echo "warning: test-tt_bench_eval skipped: its checks need pkg-config" \
	    "and FreeType's development files (libfreetype-dev)" >&2; \
	fi

# Test oracle only; the rasterizer and its normal build do not use FreeType.
build/tt-reference: tt_bench_eval/reference.c | build
	gcc -O2 -Wall -Wextra -o $@ $< $$(pkg-config --cflags --libs freetype2)

bench-tt_bench_eval: build/tt_bench_eval
	node tt_bench_eval/bench.js build/tt_bench_eval $(TT_BENCH_ARGS)

# Link profiling startup code, but leave the runtime's hot C functions
# uninstrumented. gprof then supplies PC samples, without call counters.
build/pngbench-profile-runtime.o: runtime/rt.c | build
	gcc -O2 -g -c -o $@ runtime/rt.c

build/pngbench-debug.s: build/pngbench benchmarks/pngbench-symbols.js
	node benchmarks/pngbench-symbols.js build/pngbench.s $@

build/pngbench-debug: build/pngbench-debug.s build/pngbench-profile-runtime.o
	gcc -g -o $@ build/pngbench-debug.s build/pngbench-profile-runtime.o

build/pngbench-profile: build/pngbench-debug.s build/pngbench-profile-runtime.o
	gcc -pg -no-pie -g -o $@ build/pngbench-debug.s build/pngbench-profile-runtime.o

profile-pngbench: build/pngbench-profile build/pngbench-debug
	node benchmarks/pngbench-profile.js build/pngbench-profile $(PNGBENCH_PROFILE_ARGS)

test-vim:
	node editors/vim/test.js "$(VIM_COMMAND)"

all: $(INTERPRETER) build/examples.5em build/lines.5em build/notebook.html

# Checks the types of the interpreter and the tests, which Babel can't do.
# The tests' types come from the interpreter's declarations.
typecheck: | build
	tsc --project interpreter/tsconfig.json --emitDeclarationOnly
	tsc --noEmit --project tests/tsconfig.json

$(INTERPRETER) &: $(wildcard interpreter/*.ts) interpreter/tsconfig.json | build
	$(TSC)

# The suites' types depend on the interpreter's, from its declaration files.
$(SUITES) build/test-utils.js &: $(wildcard tests/*.ts) tests/tsconfig.json $(INTERPRETER) \
    | build
	$(TEST_TSC)

build/examples.5em build/lines.5em &: docs/language.md docs/implementation.md tools/extract.awk | build
	awk -v outdir=build -f tools/extract.awk docs/language.md docs/implementation.md

# 5emelc compiled by itself, run by the interpreter
# (docs/implementation.md: Bootstrapping). The test suites build their own.
build/5emelc: compiler/5emelc.5em runtime/rt.c $(INTERPRETER) | build
	$(RUN) compiler/5emelc.5em compiler/5emelc.5em < compiler/5emelc.5em > build/5emelc.s
	gcc -O2 -o $@ build/5emelc.s runtime/rt.c

# The notebook of issue 154, one self-contained page.
build/notebook.html: notebook/page.html notebook/page.js notebook/format.js \
    notebook/worker.js tools/notebook.js build/5emel.js build/memo.js
	node tools/notebook.js $@

build:
	mkdir -p $@

clean:
	rm -rf build

.PHONY: all test typecheck bench clean compile-5emel unzip test-unzip test-vim \
    pngbench test-pngbench bench-pngbench profile-pngbench \
    tt_bench_eval test-tt_bench_eval bench-tt_bench_eval
