Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/contrib/bc/build.pkg.rig
39475 views
/*
 * *****************************************************************************
 *
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2018-2025 Gavin D. Howard and contributors.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * *****************************************************************************
 *
 * The build package file.
 *
 */

/// The path to the safe install script.
SAFE_INSTALL: str = path.join(src_dir, "scripts/safe-install.sh");

/// The file mode for executables, as an argument to the safe install script.
EXEC_INSTALL_MODE: str = "-Dm755";

/// The file mode for man pages and other files, as an argument to the safe
/// install script.
MANPAGE_INSTALL_MODE: str = "-Dm644";

// Save this.
OS: str = platform.os;

DESTDIR: str = str(config["destdir"]);

EXECPREFIX: str = str(config["execprefix"]);
EXECSUFFIX: str = str(config["execsuffix"]);

/**
 * Generates the true executable name for the given base name.
 * @param name  The base name of the executable.
 * @return      The true name of the executable, including prefix, suffix, and
                extension.
 */
fn exe_name(name: str) -> str
{
	temp: str = EXECPREFIX +~ name +~ EXECSUFFIX;
	return if OS == "Windows" { temp +~ ".exe"; } else { temp; };
}

/**
 * Generates the default executable name for the given base name.
 * @param name  The base name of the executable.
 * @return      The true name of the executable, including prefix, suffix, and
                extension.
 */
fn default_exe_name(name: str) -> str
{
	return if OS == "Windows" { name +~ ".exe"; } else { name; };
}

/**
 * Generates the true library name for the given base name.
 * @param name  The base name of the library.
 * @return      The true name of the library, including prefix and extension.
 */
fn lib_name(name: str) -> str
{
	ext: str = if OS == "Windows" { ".lib"; } else { ".a"; };
	return "lib" +~ name +~ ext;
}

BC_BIN: str = exe_name("bc");
DC_BIN: str = exe_name("dc");
LIBRARY: str = lib_name("libbcl");

BC_MANPAGE: str = EXECPREFIX +~ "bc" +~ EXECSUFFIX +~ ".1";
DC_MANPAGE: str = EXECPREFIX +~ "dc" +~ EXECSUFFIX +~ ".1";
BCL_MANPAGE: str = "bcl.3";

BCL_HEADER: str = "bcl.h";
BCL_HEADER_PATH: str = path.join(src_dir, path.join("include", BCL_HEADER));
PC_FILE: str = "bcl.pc";

/**
 * Returns the string value of the define for a prompt default define for an
 * executable.
 * @param name  The base name of the executable.
 * @return      The string value of the compiler define for the prompt default.
 */
fn prompt(name: str) -> str
{
	opt: sym = sym(config[name +~ "/default_prompt"]);

	ret: str =
	if opt == @off
	{
		"0";
	}
	else if opt == @tty_mode
	{
		str(uint(bool(config[name +~ "/default_tty_mode"])));
	}
	else
	{
		"1";
	};

	return ret;
}

HEADERS: []str = find_src_ext("include", "h");

FORCE: bool = bool(config["force"]);

BUILD_MODE: sym = sym(config["build_mode"]);

BC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @bc));
DC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @dc));
LIBRARY_ENABLED: str = str(uint(BUILD_MODE == @library));

EXTRA_MATH_ENABLED: str = str(uint(bool(config["extra_math"])));

HISTORY: sym = sym(config["history"]);
HISTORY_ENABLED: str = str(uint(HISTORY != @none));
EDITLINE_ENABLED: str = str(uint(HISTORY == @editline));
READLINE_ENABLED: str = str(uint(HISTORY == @readline));

NLS_ENABLED: str =
if OS == "Windows" || BUILD_MODE == @library
{
	"0";
}
else
{
	str(uint(sym(config["locales"]) != @none));
};

BUILD_TYPE: str =
if EXTRA_MATH_ENABLED != "0" && HISTORY_ENABLED != "0" && NLS_ENABLED != "0"
{
	"A";
}
else
{
	t: str = if EXTRA_MATH_ENABLED != "0" { ""; } else { "E"; } +~
	         if HISTORY_ENABLED != "0" { ""; } else { "H"; } +~
	         if NLS_ENABLED != "0" { ""; } else { "N"; };

	t;
};

OPTIMIZE: str = str(config["optimization"]);

VALGRIND_ARGS: []str = @[
	"valgrind",
	"--error-exitcode=100",
	"--leak-check=full",
	"--show-leak-kinds=all",
	"--errors-for-leak-kinds=all",
	"--track-fds=yes",
	"--track-origins=yes",
];

// Get the compiler. The user might have set one at the command line.
CC: str = language.compiler;

// Set optimization to "0" if it is empty.
CFLAGS_OPT: str = if OPTIMIZE == "" { "0"; } else { OPTIMIZE; };

// Get the command-line option for defining a preprocessor variable.
DEFOPT: str = compiler_db["opt.define"];

// Get the command-line string for the optimization option for the compiler.
OPTOPT: str = compiler_db["opt.optimization"] +~ CFLAGS_OPT;

// Get the compiler option for the object file to output to.
OBJOUTOPT: str = compiler_db["opt.objout"];
EXEOUTOPT: str = compiler_db["opt.exeout"];

// Get the compiler option for outputting an object file rather than an
// executable.
OBJOPT: str = compiler_db["opt.obj"];

// Get the compiler option for setting an include directory.
INCOPT: str = compiler_db["opt.include"] +~ path.join(src_dir, "include");

COVERAGE_CFLAGS: []str =
if bool(config["coverage"])
{
	@[ "-fprofile-arcs", "-ftest-coverage", "-g", "-O0", DEFOPT +~ "NDEBUG" ];
};

MAINEXEC: str =
if BUILD_MODE == @both || BUILD_MODE == @bc || BUILD_MODE == @library
{
	BC_BIN;
}
else
{
	DC_BIN;
};

MAINEXEC_FLAGS: []str = @[ DEFOPT +~ "MAINEXEC=" +~ MAINEXEC ];

// XXX: Library needs these defines to be true.
BC_DEF: str = if LIBRARY_ENABLED == "0" { BC_ENABLED; } else { "1"; };
DC_DEF: str = if LIBRARY_ENABLED == "0" { DC_ENABLED; } else { "1"; };

CFLAGS1: []str = config_list["cflags"] +~ @[ OPTOPT, INCOPT ] +~
                 COVERAGE_CFLAGS +~ MAINEXEC_FLAGS;
CFLAGS2: []str = @[
	DEFOPT +~ "BC_ENABLED=" +~ BC_DEF,
	DEFOPT +~ "DC_ENABLED=" +~ DC_DEF,
	DEFOPT +~ "BUILD_TYPE=" +~ BUILD_TYPE,
	DEFOPT +~ "EXECPREFIX=" +~ str(config["execprefix"]),
	DEFOPT +~ "BC_NUM_KARATSUBA_LEN=" +~ str(num(config["karatsuba_len"])),
	DEFOPT +~ "BC_ENABLE_LIBRARY=" +~ LIBRARY_ENABLED,
	DEFOPT +~ "BC_ENABLE_NLS=" +~ NLS_ENABLED,
	DEFOPT +~ "BC_ENABLE_EXTRA_MATH=" +~ EXTRA_MATH_ENABLED,
	DEFOPT +~ "BC_ENABLE_HISTORY=" +~ HISTORY_ENABLED,
	DEFOPT +~ "BC_ENABLE_EDITLINE=" +~ EDITLINE_ENABLED,
	DEFOPT +~ "BC_ENABLE_READLINE=" +~ READLINE_ENABLED,
	DEFOPT +~ "BC_ENABLE_MEMCHECK=" +~ str(uint(bool(config["memcheck"]))),
	DEFOPT +~ "BC_ENABLE_AFL=" +~ str(uint(bool(config["afl"]))),
	DEFOPT +~ "BC_ENABLE_OSSFUZZ=" +~ str(uint(bool(config["ossfuzz"]))),
	DEFOPT +~ "BC_DEFAULT_BANNER=" +~
	    str(uint(bool(config["bc/default_banner"]))),
	DEFOPT +~ "BC_DEFAULT_SIGINT_RESET=" +~
	    str(uint(bool(config["bc/default_sigint_reset"]))),
	DEFOPT +~ "BC_DEFAULT_TTY_MODE=" +~
	    str(uint(bool(config["bc/default_tty_mode"]))),
	DEFOPT +~ "BC_DEFAULT_PROMPT=" +~ prompt("bc"),
	DEFOPT +~ "BC_DEFAULT_EXPR_EXIT=" +~
	    str(uint(bool(config["bc/default_expr_exit"]))),
	DEFOPT +~ "BC_DEFAULT_DIGIT_CLAMP=" +~
	    str(uint(bool(config["bc/default_digit_clamp"]))),
	DEFOPT +~ "DC_DEFAULT_SIGINT_RESET=" +~
	    str(uint(bool(config["dc/default_sigint_reset"]))),
	DEFOPT +~ "DC_DEFAULT_TTY_MODE=" +~
	    str(uint(bool(config["dc/default_tty_mode"]))),
	DEFOPT +~ "DC_DEFAULT_PROMPT=" +~ prompt("dc"),
	DEFOPT +~ "DC_DEFAULT_EXPR_EXIT=" +~
	    str(uint(bool(config["dc/default_expr_exit"]))),
	DEFOPT +~ "DC_DEFAULT_DIGIT_CLAMP=" +~
	    str(uint(bool(config["dc/default_digit_clamp"]))),
];
CFLAGS: []str = CFLAGS1 +~ CFLAGS2;

LDFLAGS: []str = config_list["ldflags"];

COMMON_C_FILES: []str = @[
	"src/data.c",
	"src/num.c",
	"src/rand.c",
	"src/vector.c",
	"src/vm.c",
];

EXEC_C_FILES: []str = @[
	"src/args.c",
	"src/file.c",
	"src/lang.c",
	"src/lex.c",
	"src/main.c",
	"src/opt.c",
	"src/parse.c",
	"src/program.c",
	"src/read.c",
];

BC_C_FILES: []str = @[
	"src/bc.c",
	"src/bc_lex.c",
	"src/bc_parse.c",
];

DC_C_FILES: []str = @[
	"src/dc.c",
	"src/dc_lex.c",
	"src/dc_parse.c",
];

HISTORY_C_FILES: []str = @[
	"src/history.c",
];

LIBRARY_C_FILES: []str = @[
	"src/library.c",
];

GEN_HEADER1: str =
    "// Copyright (c) 2018-2025 Gavin D. Howard and contributors.\n" +~
    "// Licensed under the 2-clause BSD license.\n" +~
    "// *** AUTOMATICALLY GENERATED FROM ";
GEN_HEADER2: str = ". DO NOT MODIFY. ***\n\n";

GEN_LABEL1: str = "const char *";
GEN_LABEL2: str = " = \"";
GEN_LABEL3: str = "\";\n\n";
GEN_NAME1: str = "const char ";
GEN_NAME2: str = "[] = {\n";

GEN_LABEL_EXTERN1: str = "extern const char *";
GEN_LABEL_EXTERN2: str = ";\n\n";
GEN_NAME_EXTERN1: str = "extern const char ";
GEN_NAME_EXTERN2: str = "[];\n\n";

GEN_IFDEF1: str = "#if ";
GEN_IFDEF2: str = "\n";
GEN_ENDIF1: str = "#endif // ";
GEN_ENDIF2: str = "\n";

GEN_EX_START: str = "{{ A H N HN }}";
GEN_EX_END: str = "{{ end }}";

/// This is the max width to print characters to strgen files. This is to ensure
/// that lines don't go much over 80 characters.
MAX_WIDTH: usize = usize(72);

/**
 * A function to generate a C file that contains a C character array with the
 * contents of a text file. For more detail, see the `gen/strgen.c` program;
 * this function is exactly equivalent to that or should be.
 * @param input        The input file name.
 * @param output       The output file name.
 * @param exclude      True if extra math stuff should be excluded, false if
 *                     they should be included.
 * @param name         The name of the array.
 * @param label        If not equal to "", this is the label for the array,
 *                     which is essentially the "file name" in `bc` and `dc`.
 * @param define       If not equal to "", this is the preprocessor define
 *                     expression that should be used to guard the array with a
 *                     `#if`/`#endif` combo.
 * @param remove_tabs  True if tabs should be ignored, false if they should be
 *                     included.
 */
fn strgen(
	input: str,
	output: str,
	exclude: bool,
	name: str,
	label: str,
	def: str,
	remove_tabs: bool,
) -> void
{
	in: str = io.read_file(input);

	io.open(output, "w"): f
	{
		f.print(GEN_HEADER1 +~ input +~ GEN_HEADER2);

		if label != ""
		{
			f.print(GEN_LABEL_EXTERN1 +~ label +~ GEN_LABEL_EXTERN2);
		}

		f.print(GEN_NAME_EXTERN1 +~ name +~ GEN_NAME_EXTERN2);

		if def != ""
		{
			f.print(GEN_IFDEF1 +~ def +~ GEN_IFDEF2);
		}

		if label != ""
		{
			f.print(GEN_LABEL1 +~ label +~ GEN_LABEL2 +~ name +~ GEN_LABEL3);
		}

		f.print(GEN_NAME1 +~ name +~ GEN_NAME2);

		i: !usize = usize(0);
		count: !usize = usize(0);
		slashes: !usize = usize(0);

		// This is where the end of the license comment is found.
		while slashes < 2 && in[i] > 0
		{
			if slashes == 1 && in[i] == '*' && in[i + 1] == '/' &&
			   (in[i + 2] == '\n' || in[i + 2] == '\r')
			{
				slashes! = slashes + usize(1);
				i! = i + usize(2);
			}
			else if slashes == 0 && in[i] == '/' && in[i + 1] == '*'
			{
				slashes! = slashes + usize(1);
				i! = i + usize(1);
			}

			i! = i + usize(1);
		}

		// The file is invalid if the end of the license comment could not be
		// found.
		if i == in.len
		{
			error("Could not find end of license comment");
		}

		i! = i + usize(1);

		// Do not put extra newlines at the beginning of the char array.
		while in[i] == '\n' || in[i] == '\r'
		{
			i! = i + usize(1);
		}

		// This loop is what generates the actual char array. It counts how many
		// chars it has printed per line in order to insert newlines at
		// appropriate places. It also skips tabs if they should be removed.
		while i < in.len
		{
			if in[i] == '\r'
			{
				i! = i + usize(1);
				continue;
			}

			// If we should output the character, i.e., it is not a tab or we
			// can remove tabs...
			if !remove_tabs || in[i] != '\t'
			{
				// Check for excluding something for extra math.
				if in[i] == '{'
				{
					if i + GEN_EX_START.len <= in.len &&
					   in.slice(i, i + GEN_EX_START.len) == GEN_EX_START
					{
						if exclude
						{
							// Get past the braces.
							i! = i + usize(2);

							// Find the end of the end.
							while in[i] != '{' &&
							      in.slice(i, i + GEN_EX_END.len) != GEN_EX_END
							{
								i! = i + usize(1);
							}

							i! = i + GEN_EX_END.len;

							// Skip the last newline.
							if in[i] == '\r'
							{
								i! = i + usize(1);
							}

							i! = i + usize(1);

							continue;
						}
					}
					else if !exclude &&
					        in.slice(i, i + GEN_EX_END.len) == GEN_EX_END
					{
						i! = i + GEN_EX_END.len;

						// Skip the last newline.
						if in[i] == '\r'
						{
							i! = i + usize(1);
						}

						i! = i + usize(1);

						continue;
					}
				}

				// Print a tab if we are at the beginning of a line.
				if count == 0
				{
					f.print("\t");
				}

				val: str = str(in[i]) +~ ",";

				// Print the character.
				f.print(val);

				// Adjust the count.
				count! = count + val.len;

				if count > MAX_WIDTH
				{
					count! = usize(0);
					f.print("\n");
				}
			}

			i! = i + usize(1);
		}

		// Make sure the end looks nice.
		if count == 0
		{
			f.print("  ");
		}

		// Insert the NUL byte at the end.
		f.print("0\n};\n");

		if def != ""
		{
			f.print(GEN_ENDIF1 +~ def +~ GEN_ENDIF2);
		}
	}
}

/**
 * Creates a target to generate an object file from the given C file and returns
 * the target name of the new target.
 * @param c_file  The name of the C file target.
 * @return        The name of the object file target.
 */
fn c2o(c_file: str) -> str
{
	o_file: str = c_file +~ (if OS == "Windows" { ".obj"; } else { ".o"; });

	target o_file: c_file, HEADERS
	{
		$ $CC %(config_list["other_cflags"]) %(CFLAGS) $OBJOPT $OBJOUTOPT @(tgt)
		  @(file_dep);
	}

	return o_file;
}

/**
 * Generates a target to turn a text file into a C file with the text file's
 * contents as a char array, then generates a target to generate an object file
 * from that C file, then returns the name of the object file target.
 * @param txt_file     The name of the text file.
 * @param name         The name of the char array in the C file.
 * @param label        The label for the array, if any. (See the @a strgen()
 *                     function for more information.)
 * @param def          The preprocessor define(s) to guard the array, if any.
 *                     (See the @a strgen() function for more information.)
 * @param remove_tabs  True if tabs should be ignored, false otherwise. (See the
 *                     @a strgen() function for more information.)
 * @return             The name of the object file target.
 */
fn txt2o(
	txt_file: str,
	name: str,
	label: str,
	def: str,
	remove_tabs: bool,
) -> str
{
	c_file: str = txt_file +~ ".c";

	c_config: Gaml = @(gaml){
		strgen_name: $name
		strgen_label: $label
		strgen_define: $def
		strgen_remove_tabs: $remove_tabs
	};

	push c_config: config_stack
	{
		target c_file: txt_file
		{
			strgen(file_dep, tgt, EXTRA_MATH_ENABLED == "0",
			       str(config["strgen_name"]), str(config["strgen_label"]),
			       str(config["strgen_define"]),
			       bool(config["strgen_remove_tabs"]));
		}
	}

	return c2o(c_file);
}

/**
 * Generates a target for an executable and returns its name.
 * @param name     The name of the executable.
 * @param o_files  The object files for the executable.
 * @return         The name of the generated target.
 */
fn exe(name: str, o_files: []str) -> void
{
	target name: o_files
	{
		$ $CC %(config_list["other_cflags"]) %(config_list["strip_flag"])
		  %(CFLAGS) %(LDFLAGS) $EXEOUTOPT @(tgt) %(file_deps);
	}
}

/**
 * Generates a target for a link.
 * @param name  The name of the link.
 * @param exec  The name of the executable target.
 */
fn ln(name: str, exec: str) -> void
{
	if OS == "Windows"
	{
		target name: exec
		{
			$ copy /v /y /b @(file_dep) @(tgt);
		}
	}
	else
	{
		target name: exec
		{
			$ ln -fs @("./" +~ path.basename(file_dep)) @(tgt);
		}
	}
}

/**
 * Generates a target for a library.
 * @param name  The name of the library.
 * @param exec  The name of the executable target.
 */
fn lib(name: str, o_files: []str) -> void
{
	if OS == "WINDOWS"
	{
		exe(name, o_files);
	}
	else
	{
		target name: o_files
		{
			$ ar -r -cu @(tgt) %(file_deps);
		}
	}
}

fn check_err_test(
	name: str,
	res: CmdResult,
) -> void
{
	if res.exitcode > 127
	{
		error("Test \"" +~ name +~ "\" crashed");
	}

	if res.exitcode == 0
	{
		error("Test \"" +~ name +~ "\" returned no error");
	}

	if res.exitcode == 100
	{
		error("Test \"" +~ name +~ "\" had memory errors on non-fatal error\n");
	}

	if res.stderr.len <= 1
	{
		error("Test \"" +~ name +~ "\" produced no error message");
	}
}

fn check_test_retcode(
	name: str,
	exitcode: uint,
) -> void
{
	if exitcode != 0
	{
		error("Test \"" +~ name +~ "\" failed with exitcode: " +~
		      str(exitcode) +~ "\n");
	}
}

fn check_test(
	name: str,
	res: CmdResult,
	exp_path: str,
) -> void
{
	check_test_retcode(name, res.exitcode);

	exp := io.read_file_bytes(exp_path);

	if exp != res.stdout_full
	{
		error("Test \"" +~ name +~ "\" failed\n" +~ str(res.stderr));
	}
}

fn register_standard_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
	extra: bool,
) -> void
{
	all_file: str = path.join(src_testdir, "all.txt");
	tests: []str = io.read_file(all_file).split("\n");

	extra_path := path.join(src_dir, "tests/extra_required.txt");
	extra_required: []str = io.read_file(extra_path).split("\n");

	for t: tests
	{
		if t == ""
		{
			continue;
		}

		// Skip extra math tests if it is not enabled.
		if !extra && extra_required contains t
		{
			continue;
		}

		test sym(path.join(testdir, t)): bin
		{
			halt: str = str(config["halt"]);

			name: str = path.basename(tgt_name);
			testdir: str = path.dirname(tgt_name);
			calc: str = path.basename(testdir);

			test_file: str = tgt_name +~ ".txt";
			test_result_file: str = tgt_name +~ "_results.txt";

			src_test_file: str = path.join(src_dir, test_file);
			src_test_result_file: str = path.join(src_dir, test_result_file);

			actual_test_file: str =
			if !path.isfile(src_test_file)
			{
				// If we shouldn't generate tests, skip.
				if !bool(config["generated_tests"])
				{
					io.eprint("Skipping test " +~ tgt_name +~ "\n");
					return;
				}

				script_name: str = name +~ "." +~ calc;

				scriptdir: str = path.join(testdir, "scripts");
				src_scriptdir: str = path.join(src_dir, scriptdir);
				src_script_name: str = path.join(src_scriptdir, script_name);

				$ @(default_exe_name(calc)) $src_script_name > $test_file;

				test_file;
			}
			else
			{
				src_test_file;
			};

			exp_result_file: str =
			if !path.isfile(src_test_result_file)
			{
				tmpfile: str = path.tmp(calc +~ "_test_result");

				$ @(default_exe_name(calc)) %(config_list["gen_options"])
				  $actual_test_file << $halt > $tmpfile;

				tmpfile;
			}
			else
			{
				src_test_result_file;
			};

			res := $ %(config_list["args"]) %(config_list["options"])
			         $actual_test_file << $halt;

			check_test(tgt_name, res, exp_result_file);
		}
	}
}

fn register_script_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
	extra: bool,
) -> void
{
	scriptdir: str = path.join(testdir, "scripts");
	src_scriptdir: str = path.join(src_testdir, "scripts");
	all_file: str = path.join(src_scriptdir, "all.txt");
	tests: []str = io.read_file(all_file).split("\n");

	for t: tests
	{
		if t == ""
		{
			continue;
		}

		// Skip extra math tests if it is not enabled.
		if !extra && (t == "rand.bc" || t == "root.bc" || t == "i2rand.bc")
		{
			continue;
		}

		test sym(path.join(scriptdir, t)): bin
		{
			halt: str = str(config["halt"]);

			name: str = path.basename(tgt_name);
			testdir: str = path.dirname(tgt_name);
			testdir2: str = path.dirname(testdir);
			calc: str = path.basename(testdir2);

			test_file: str = tgt_name;
			test_file_dir: str = path.dirname(tgt_name);
			test_file_name: str = path.basename(tgt_name, "." +~ calc);
			test_result_file: str = path.join(test_file_dir,
			                                  test_file_name +~ ".txt");

			src_test_file: str = path.join(src_dir, test_file);
			src_test_result_file: str = path.join(src_dir, test_result_file);

			exp_result_file: str =
			if !path.isfile(src_test_result_file)
			{
				tmpfile: str = path.tmp(calc +~ "_script_test_result");

				// This particular test needs to be generated straight. Also, on
				// Windows, we don't have `sed`, and the `bc`/`dc` there is
				// probably this one anyway.
				if name == "stream.dc" || host.os == "Windows"
				{
					$ @(default_exe_name(calc)) $src_test_file << $halt
					  > $tmpfile;
				}
				else
				{
					root_testdir: str = path.join(src_dir, "tests");

					// This sed and the script are to remove an incompatibility
					// with GNU bc, where GNU bc is wrong. See the development
					// manual (manuals/development.md#script-tests) for more
					// information.
					$ @(default_exe_name(calc)) $src_test_file << $halt |
					  sed -n -f @(path.join(root_testdir, "script.sed"))
					  > $tmpfile;
				}

				tmpfile;
			}
			else
			{
				src_test_result_file;
			};

			if calc == "bc"
			{
				res1 := $ %(config_list["args"]) -g
				          %(config_list["script_options"]) $src_test_file
				          << $halt;

				check_test(tgt_name, res1, exp_result_file);
			}

			// These tests do not need to run without global stacks.
			if name == "globals.bc" || name == "references.bc" ||
			   name == "rand.bc"
			{
				return;
			}

			res2 := $ %(config_list["args"]) %(config_list["script_options"])
			          $src_test_file << $halt;

			check_test(tgt_name, res2, exp_result_file);
		}
	}
}

fn register_stdin_test(
	bin: str,
	testdir: str,
	name: str
) -> void
{
	test sym(path.join(testdir, name)): bin
	{
		name: str = path.basename(tgt_name);
		testdir: str = path.dirname(tgt_name);
		calc: str = path.basename(testdir);

		halt: str = if name == "bc" { "halt"; } else { "q"; };

		test_file: str = tgt_name +~ ".txt";
		test_result_file: str = tgt_name +~ "_results.txt";

		src_test_file: str = path.join(src_dir, test_file);
		src_test_result_file: str = path.join(src_dir, test_result_file);

		res := $ %(config_list["args"]) %(config_list["options"])
		         < $src_test_file;

		check_test(tgt_name, res, src_test_result_file);
	}
}

fn register_stdin_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
) -> void
{
	calc: str = path.basename(testdir);

	if calc == "bc"
	{
		for t: @[ "stdin", "stdin1", "stdin2" ]
		{
			register_stdin_test(bin, testdir, t);
		}
	}
	else
	{
		// dc only needs one.
		register_stdin_test(bin, testdir, "stdin");
	}
}

fn register_read_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
) -> void
{
	calc: str = path.basename(testdir);

	read_call: str = if calc == "bc" { "read()"; } else { "?"; };
	read_expr: str =
	if calc == "bc"
	{
		read_call +~ "\n5+5;";
	}
	else
	{
		read_call;
	};
	read_multiple: str =
	if calc == "bc"
	{
		"3\n2\n1\n";
	}
	else
	{
		"3pR\n2pR\n1pR\n";
	};

	read_test_config: Gaml = @(gaml){
		read_call: $read_call
		read_expr: $read_expr
		read_multiple: $read_multiple
	};

	push read_test_config: config_stack
	{
		// First test is the regular read test.
		test sym(path.join(testdir, "read")): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			test_file: str = tgt_name +~ ".txt";
			src_test_file: str = path.join(src_dir, test_file);

			read_call: str = str(config["read_call"]);

			lines: []str = io.read_file(src_test_file).split("\n");

			for l: lines
			{
				if l == ""
				{
					continue;
				}

				res := $ %(config_list["args"]) %(config_list["options"])
				         << @(read_call +~ "\n" +~ l +~ "\n");

				check_test(tgt_name, res,
				           path.join(src_testdir, "read_results.txt"));
			}
		}

		// Next test is reading multiple times.
		test sym(path.join(testdir, "read_multiple")): bin
		{
			testdir: str = path.dirname(tgt_name);

			test_file: str = tgt_name +~ ".txt";

			path.mkdirp(path.dirname(test_file));

			read_call: str = str(config["read_call"]);

			exp_path: str = path.tmp("read_multiple_results");

			io.open(exp_path, "w"): f
			{
				f.print("3\n2\n1\n");
			}

			res := $ %(config_list["args"]) %(config_list["options"])
			         -e $read_call -e $read_call -e $read_call
			         << @(str(config["read_multiple"]));

			check_test(tgt_name, res, exp_path);
		}

		// Next test is the read errors test.
		test sym(path.join(testdir, "read_errors")): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			test_file: str = tgt_name +~ ".txt";
			src_test_file: str = path.join(src_dir, test_file);

			path.mkdirp(path.dirname(test_file));

			read_call: str = str(config["read_call"]);

			lines: []str = io.read_file(src_test_file).split("\n");

			for l: lines
			{
				if l == ""
				{
					continue;
				}

				res := $ %(config_list["args"]) %(config_list["options"])
				         << @(read_call +~ "\n" +~ l +~ "\n");

				check_err_test(tgt_name, res);
			}
		}

		// Next test is the empty read test.
		test sym(path.join(testdir, "read_empty")): bin
		{
			read_call: str = str(config["read_call"]);

			res := $ %(config_list["args"]) %(config_list["options"])
			         << @(read_call +~ "\n");

			check_err_test(tgt_name, res);
		}

		// Next test is the read EOF test.
		test sym(path.join(testdir, "read_EOF")): bin
		{
			read_call: str = str(config["read_call"]);

			res := $ %(config_list["args"]) %(config_list["options"])
			         << $read_call;

			check_err_test(tgt_name, res);
		}
	}
}

fn run_error_lines_test(name: str) -> void
{
	file: str = path.join(src_dir, name);

	lines: []str = io.read_file(file).split("\n");

	for l: lines
	{
		if l == ""
		{
			continue;
		}

		res := $ %(config_list["args"]) %(config_list["options"])
		         %(config_list["error_options"]) << @(l +~ "\n");

		check_err_test(name, res);
	}
}

fn register_error_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
) -> void
{
	calc: str = path.basename(testdir);

	// First test is command-line expression error.
	test sym(path.join(testdir, "command-line_expr_error")): bin
	{
		halt: str = str(config["halt"]);

		res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
		         -e "2+2" << $halt;

		check_err_test(tgt_name, res);
	}

	// First test is command-line file expression error.
	test sym(path.join(testdir, "command-line_file_expr_error")): bin
	{
		testdir: str = path.dirname(tgt_name);
		halt: str = str(config["halt"]);

		res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
		         -f @(path.join(testdir, "decimal.txt")) << $halt;

		check_err_test(tgt_name, res);
	}

	if calc == "bc"
	{
		test sym(path.join(testdir, "posix_warning")): bin
		{
			res := $ %(config_list["args"]) %(config_list["options"]) -w
			         << @("line");

			if res.exitcode != 0
			{
				error("Test \"" +~ tgt_name +~ "\" returned an error (" +~
				      str(res.exitcode) +~ ")");
			}

			output: str = str(res.stderr);

			if output == "" || output == "\n"
			{
				error("Test \"" +~ tgt_name +~ "\" did not print a warning");
			}
		}

		test sym(path.join(testdir, "posix_errors.txt")): bin
		{
			run_error_lines_test(tgt_name);
		}
	}

	test sym(path.join(testdir, "errors.txt")): bin
	{
		run_error_lines_test(tgt_name);
	}

	errors_dir: str = path.join(testdir, "errors");

	for f: find_src_ext(errors_dir, "txt")
	{
		// Skip the problematic test, if requested.
		if calc == "bc" && f contains "33.txt" &&
		   !bool(config["problematic_tests"])
		{
			continue;
		}

		test sym(f): bin
		{
			errors_dir: str = path.dirname(tgt_name);
			testdir: str = path.dirname(errors_dir);
			calc: str = path.basename(testdir);

			halt: str = str(config["halt"]);

			res1 := $ %(config_list["args"]) %(config_list["error_options"]) -c
			          @(tgt_name) << $halt;

			check_err_test(tgt_name, res1);

			res2 := $ %(config_list["args"]) %(config_list["error_options"]) -C
			          @(tgt_name) << $halt;

			check_err_test(tgt_name, res2);

			res3 := $ %(config_list["args"]) %(config_list["error_options"]) -c
			          < @(path.join(src_dir, tgt_name));

			check_err_test(tgt_name, res3);

			res4 := $ %(config_list["args"]) %(config_list["error_options"]) -C
			          < @(path.join(src_dir, tgt_name));

			check_err_test(tgt_name, res4);
		}
	}
}

fn check_kwredef_test(
	name: str,
	res: CmdResult,
) -> void
{
	testdir: str = path.dirname(name);
	redefine_exp: str = path.join(testdir, "redefine_exp.txt");

	check_test(tgt_name, res, redefine_exp);
}

OTHER_LINE_LEN_RESULTS_NAME: str = "line_length_test_results.txt";
OTHER_LINE_LEN70_RESULTS_NAME: str = "line_length70_test_results.txt";
OTHER_MATHLIB_SCALE_RESULTS_NAME: str = "mathlib_scale_results.txt";

fn register_other_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
	extra: bool,
) -> void
{
	calc: str = path.basename(testdir);

	path.mkdirp(testdir);

	// Halt test.
	test sym(path.join(testdir, "halt")): bin
	{
		halt: str = str(config["halt"]) +~ "\n";

		res := $ %(config_list["args"]) << $halt;

		check_test_retcode(tgt_name, res.exitcode);
	}

	if calc == "bc"
	{
		// bc has two halt or quit commands, so test the second as well.
		test sym(path.join(testdir, "quit")): bin
		{
			res := $ %(config_list["args"]) << @("quit\n");

			check_test_retcode(tgt_name, res.exitcode);
		}

		// Also, make sure quit only quits after an expression.
		test sym(path.join(testdir, "quit_after_expr")): bin
		{
			res := $ %(config_list["args"]) -e "1+1" << @("quit\n");

			check_test_retcode(tgt_name, res.exitcode);

			if str(res.stdout) != "2"
			{
				error("Test \"" +~ tgt_name +~
				      "\" did not have the right output");
			}
		}

		test sym(path.join(testdir, "env_args1")): bin
		{
			env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
			{
				res := $ %(config_list["args"]) << @("s(.02893)\n");

				check_test_retcode(tgt_name, res.exitcode);
			}
		}

		test sym(path.join(testdir, "env_args2")): bin
		{
			env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
			{
				res := $ %(config_list["args"]) -e 4 << @("halt\n");

				check_test_retcode(tgt_name, res.exitcode);
			}
		}

		redefine_exp: str = path.join(testdir, "redefine_exp.txt");

		io.open(redefine_exp, "w"): f
		{
			f.print("5\n0\n");
		}

		test sym(path.join(testdir, "keyword_redefinition1")): bin
		{
			res := $ %(config_list["args"]) --redefine=print -e
			         "define print(x) { x }" -e "print(5)" << @("halt\n");

			check_kwredef_test(tgt_name, res);
		}

		test sym(path.join(testdir, "keyword_redefinition2")): bin
		{
			res := $ %(config_list["args"]) -r abs -r else -e
			         "abs = 5; else = 0" -e "abs;else" << @("halt\n");

			check_kwredef_test(tgt_name, res);
		}

		if extra
		{
			test sym(path.join(testdir, "keyword_redefinition_lib2")): bin
			{
				res := $ %(config_list["args"]) -lr abs -e "perm(5, 1)" -e 0
				         << @("halt\n");

				check_kwredef_test(tgt_name, res);
			}

			test sym(path.join(testdir, "leading_zero_script")): bin
			{
				testdir: str = path.dirname(tgt_name);
				src_testdir: str = path.join(src_dir, testdir);

				res := $ %(config_list["args"]) -lz
				         @(path.join(src_testdir, "leadingzero.txt"))
				         << @(str(config["halt"]));

				check_test(tgt_name, res,
				           path.join(src_testdir, "leadingzero_results.txt"));
			}
		}

		test sym(path.join(testdir, "keyword_redefinition3")): bin
		{
			res := $ %(config_list["args"]) -r abs -r else -e
			         "abs = 5; else = 0" -e "abs;else" << @("halt\n");

			check_kwredef_test(tgt_name, res);
		}

		test sym(path.join(testdir, "keyword_redefinition_error")): bin
		{
			res := $ %(config_list["args"]) -r break -e "define break(x) { x }";

			check_err_test(tgt_name, res);
		}

		test sym(path.join(testdir,
		                   "keyword_redefinition_without_redefine")): bin
		{
			res := $ %(config_list["args"]) -e "define read(x) { x }";

			check_err_test(tgt_name, res);
		}

		test sym(path.join(testdir, "multiline_comment_in_expr_file")): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			// tests/bc/misc1.txt happens to have a multiline comment in it.
			src_test_file: str = path.join(src_testdir, "misc1.txt");
			src_test_results_file: str = path.join(src_testdir,
			                                       "misc1_results.txt");

			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");

			check_test(tgt_name, res, src_test_results_file);
		}

		test sym(path.join(testdir,
		                   "multiline_comment_error_in_expr_file")): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			src_test_file: str = path.join(src_testdir, "errors/05.txt");

			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");

			check_err_test(tgt_name, res);
		}

		test sym(path.join(testdir, "multiline_string_in_expr_file")): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			// tests/bc/strings.txt happens to have a multiline string in it.
			src_test_file: str = path.join(src_testdir, "strings.txt");
			src_test_results_file: str = path.join(src_testdir,
			                                       "strings_results.txt");

			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");

			check_test(tgt_name, res, src_test_results_file);
		}

		tst := path.join(testdir,
		                 "multiline_string_with_backslash_error_in_expr_file");

		test sym(tst): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			src_test_file: str = path.join(src_testdir, "errors/16.txt");

			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");

			check_err_test(tgt_name, res);
		}

		tst2 := path.join(testdir, "multiline_string_error_in_expr_file");

		test sym(tst2): bin
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			src_test_file: str = path.join(src_testdir, "errors/04.txt");

			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");

			check_err_test(tgt_name, res);
		}

		test sym(path.join(testdir, "interactive_halt")): bin
		{
			res := $ %(config_list["args"]) -i << @("halt\n");

			check_test_retcode(tgt_name, res.exitcode);
		}
	}
	else
	{
		test sym(path.join(testdir, "env_args1")): bin
		{
			env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
			{
				res := $ %(config_list["args"]) << @("4s stuff\n");

				check_test_retcode(tgt_name, res.exitcode);
			}
		}

		test sym(path.join(testdir, "env_args2")): bin
		{
			env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
			{
				res := $ %(config_list["args"]) -e 4pR;

				check_test_retcode(tgt_name, res.exitcode);
			}
		}

		test sym(path.join(testdir, "extended_register_command1")): bin
		{
			testdir: str = path.dirname(tgt_name);
			results: str = tgt_name +~ ".txt";

			path.mkdirp(testdir);

			io.open(results, "w"): f
			{
				f.print("0\n");
			}

			res := $ %(config_list["args"]) -e gxpR << @("q\n");

			check_test(tgt_name, res, results);
		}

		test sym(path.join(testdir, "extended_register_command2")): bin
		{
			testdir: str = path.dirname(tgt_name);
			results: str = tgt_name +~ ".txt";

			path.mkdirp(testdir);

			io.open(results, "w"): f
			{
				f.print("1\n");
			}

			res := $ %(config_list["args"]) -x -e gxpR << @("q\n");

			check_test(tgt_name, res, results);
		}
	}

	path.mkdirp(testdir);

	other_tests_results: []str = config_list["other_tests_results"];

	io.open(path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME), "w"): f
	{
		f.print(other_tests_results[0] +~ "\n");
	}

	io.open(path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME), "w"): f
	{
		f.print(other_tests_results[1] +~ "\n");
	}

	test sym(path.join(testdir, "line_length1")): bin
	{
		env.set env.str(str(config["var"]), "80")
		{
			testdir: str = path.dirname(tgt_name);

			other_tests: []str = config_list["other_tests"];

			res := $ %(config_list["args"]) << @(other_tests[3]);

			check_test(tgt_name, res,
			           path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
		}
	}

	test sym(path.join(testdir, "line_length2")): bin
	{
		env.set env.str(str(config["var"]), "2147483647")
		{
			testdir: str = path.dirname(tgt_name);

			other_tests: []str = config_list["other_tests"];

			res := $ %(config_list["args"]) << @(other_tests[3]);

			check_test(tgt_name, res,
			           path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME));
		}
	}

	test sym(path.join(testdir, "expr_and_file_args_test")): bin
	{
		testdir: str = path.dirname(tgt_name);
		src_testdir: str = path.join(src_dir, testdir);

		input_file: str = path.join(src_testdir, "add.txt");
		input: str = io.read_file(input_file);
		results_file: str = path.join(src_testdir, "add_results.txt");
		results: str = io.read_file(results_file);

		output_file: str = path.join(testdir, "expr_file_args.txt");

		io.open(output_file, "w"): f
		{
			f.print(results +~ results +~ results +~ results);
		}

		res := $ %(config_list["args"]) -e $input -f $input_file
		         --expression $input --file $input_file
		         -e @(str(config["halt"]));

		check_test(tgt_name, res, output_file);
	}

	test sym(path.join(testdir, "files_test")): bin
	{
		env.set env.str(str(config["var"]), "2147483647")
		{
			testdir: str = path.dirname(tgt_name);
			src_testdir: str = path.join(src_dir, testdir);

			input_file: str = path.join(src_testdir, "add.txt");
			input: str = io.read_file(input_file);
			results_file: str = path.join(src_testdir, "add_results.txt");
			results: str = io.read_file(results_file);

			output_file: str = path.join(testdir, "files.txt");

			io.open(output_file, "w"): f
			{
				f.print(results +~ results +~ results +~ results);
			}

			res := $ %(config_list["args"]) -- $input_file $input_file
			         $input_file $input_file << @(str(config["halt"]));

			check_test(tgt_name, res, output_file);
		}
	}

	test sym(path.join(testdir, "line_length3")): bin
	{
		env.set env.str(str(config["var"]), "62")
		{
			testdir: str = path.dirname(tgt_name);

			other_tests: []str = config_list["other_tests"];

			res := $ %(config_list["args"]) -L << @(other_tests[3]);

			check_test(tgt_name, res,
			           path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
		}
	}

	test sym(path.join(testdir, "line_length_func")): bin
	{
		env.set env.str(str(config["var"]), "62")
		{
			testdir: str = path.dirname(tgt_name);
			results: str = tgt_name +~ ".txt";

			path.mkdirp(testdir);

			io.open(results, "w"): f
			{
				f.print("0\n");
			}

			other_tests: []str = config_list["other_tests"];

			res := $ %(config_list["args"]) -L << @(other_tests[2]);

			check_test(tgt_name, res, results);
		}
	}

	test sym(path.join(testdir, "arg")): bin
	{
		halt: str = str(config["halt"]);

		res1 := $ %(config_list["args"]) -h << $halt;
		check_test_retcode(tgt_name, res1.exitcode);

		res2 := $ %(config_list["args"]) -P << $halt;
		check_test_retcode(tgt_name, res2.exitcode);

		res3 := $ %(config_list["args"]) -R << $halt;
		check_test_retcode(tgt_name, res3.exitcode);

		res4 := $ %(config_list["args"]) -v << $halt;
		check_test_retcode(tgt_name, res4.exitcode);

		res5 := $ %(config_list["args"]) -V << $halt;
		check_test_retcode(tgt_name, res5.exitcode);
	}

	test sym(path.join(testdir, "leading_zero_arg")): bin
	{
		testdir: str = path.dirname(tgt_name);
		calc: str = path.basename(testdir);

		expected_file: str = tgt_name +~ ".txt";

		expected: str = "0.1\n-0.1\n1.1\n-1.1\n0.1\n-0.1\n";

		io.open(expected_file, "w"): f
		{
			f.print(expected);
		}

		data: str =
		if calc == "bc"
		{
			"0.1\n-0.1\n1.1\n-1.1\n.1\n-.1\n";
		}
		else
		{
			"0.1pR\n_0.1pR\n1.1pR\n_1.1pR\n.1pR\n_.1pR\n";
		};

		res := $ %(config_list["args"]) -z << $data;

		check_test(tgt_name, res, expected_file);
	}

	test sym(path.join(testdir, "invalid_file_arg")): bin
	{
		res := $ %(config_list["args"]) -f
		         "astoheusanotehynstahonsetihaotsnuhynstahoaoetusha.txt";

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "invalid_option_arg")): bin
	{
		other_tests: []str = config_list["other_tests"];

		res := $ %(config_list["args"]) @("-" +~ other_tests[0])
		         -e @(str(config["halt"]));

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "invalid_long_option_arg")): bin
	{
		other_tests: []str = config_list["other_tests"];

		res := $ %(config_list["args"]) @("--" +~ other_tests[1])
		         -e @(str(config["halt"]));

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "unrecognized_option_arg")): bin
	{
		res := $ %(config_list["args"]) -u -e @(str(config["halt"]));

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "unrecognized_long_option_arg")): bin
	{
		res := $ %(config_list["args"]) --uniform -e @(str(config["halt"]));

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "no_required_arg_for_option")): bin
	{
		res := $ %(config_list["args"]) -f;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "no_required_arg_for_long_option")): bin
	{
		res := $ %(config_list["args"]) --file;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "given_arg_for_long_option_with_no_arg")): bin
	{
		res := $ %(config_list["args"]) --version=5;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "colon_option")): bin
	{
		res := $ %(config_list["args"]) -:;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "colon_long_option")): bin
	{
		res := $ %(config_list["args"]) --:;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "builtin_variable_arg_test")): bin
	{
		testdir: str = path.dirname(tgt_name);
		calc: str = path.basename(testdir);

		extra: bool = bool(config["extra_math"]);

		output: str =
		if extra
		{
			"14\n15\n16\n17.25\n";
		}
		else
		{
			"14\n15\n16\n";
		};

		output_file: str = tgt_name +~ ".txt";

		io.open(output_file, "w"): f
		{
			f.print(output);
		}

		data: str =
		if extra
		{
			if calc == "bc"
			{
				"s=scale;i=ibase;o=obase;t=seed@2;ibase=A;obase=A;s;i;o;t;";
			}
			else
			{
				"J2@OIKAiAopRpRpRpR";
			}
		}
		else
		{
			if calc == "bc"
			{
				"s=scale;i=ibase;o=obase;ibase=A;obase=A;s;i;o;";
			}
			else
			{
				"OIKAiAopRpRpR";
			}
		};

		args: []str =
		if extra
		{
			@[ "-S14", "-I15", "-O16", "-E17.25" ];
		}
		else
		{
			@[ "-S14", "-I15", "-O16" ];
		};

		res1 := $ %(config_list["args"]) %(args) << $data;
		check_test(tgt_name, res1, output_file);

		long_args: []str =
		if extra
		{
			@[ "--scale=14", "--ibase=15", "--obase=16", "--seed=17.25" ];
		}
		else
		{
			@[ "--scale=14", "--ibase=15", "--obase=16" ];
		};

		res2 := $ %(config_list["args"]) %(long_args) << $data;
		check_test(tgt_name, res2, output_file);
	}

	if calc == "bc"
	{
		io.open(path.join(testdir, OTHER_MATHLIB_SCALE_RESULTS_NAME), "w"): f
		{
			f.print("100\n");
		}

		test sym(path.join(testdir, "builtin_var_arg_with_lib")): bin
		{
			testdir: str = path.dirname(tgt_name);
			results_file: str = path.join(testdir,
			                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

			res := $ %(config_list["args"]) -S100 -l << @("scale\n");

			check_test(tgt_name, res, results_file);
		}

		test sym(path.join(testdir, "builtin_variable_long_arg_with_lib")): bin
		{
			testdir: str = path.dirname(tgt_name);
			results_file: str = path.join(testdir,
			                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

			res := $ %(config_list["args"]) --scale=100 --mathlib <<
			         @("scale\n");

			check_test(tgt_name, res, results_file);
		}

		test sym(path.join(testdir, "builtin_var_arg_with_lib_env_arg")): bin
		{
			env.set env.str("BC_ENV_ARGS", "-l")
			{
				testdir: str = path.dirname(tgt_name);
				results_file: str = path.join(testdir,
				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

				res := $ %(config_list["args"]) -S100 << @("scale\n");

				check_test(tgt_name, res, results_file);
			}
		}

		test sym(path.join(testdir,
			               "builtin_var_long_arg_with_lib_env_arg")): bin
		{
			env.set env.str("BC_ENV_ARGS", "-l")
			{
				testdir: str = path.dirname(tgt_name);
				results_file: str = path.join(testdir,
				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

				res := $ %(config_list["args"]) --scale=100 << @("scale\n");

				check_test(tgt_name, res, results_file);
			}
		}

		test sym(path.join(testdir, "builtin_var_env_arg_with_lib_arg")): bin
		{
			env.set env.str("BC_ENV_ARGS", "-S100")
			{
				testdir: str = path.dirname(tgt_name);
				results_file: str = path.join(testdir,
				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

				res := $ %(config_list["args"]) -l << @("scale\n");

				check_test(tgt_name, res, results_file);
			}
		}

		test sym(path.join(testdir,
		                   "builtin_var_long_env_arg_with_lib_arg")): bin
		{
			env.set env.str("BC_ENV_ARGS", "--scale=100")
			{
				testdir: str = path.dirname(tgt_name);
				results_file: str = path.join(testdir,
				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);

				res := $ %(config_list["args"]) -l << @("scale\n");

				check_test(tgt_name, res, results_file);
			}
		}

		test sym(path.join(testdir, "limits")): bin
		{
			res := $ %(config_list["args"]) << @("limits\n");

			check_test_retcode(tgt_name, res.exitcode);

			if str(res.stdout) == "" || str(res.stdout) == "\n"
			{
				error("Test \"" +~ tgt_name +~ "\" did not produce output");
			}
		}
	}

	test sym(path.join(testdir, "bad_arg_for_builtin_var_option")): bin
	{
		testdir: str = path.dirname(tgt_name);
		calc: str = path.basename(testdir);

		scale: str = if calc == "bc" { "scale\n"; } else { "K\n"; };

		res1 := $ %(config_list["args"]) --scale=18923c.rlg << $scale;

		check_err_test(tgt_name, res1);

		if bool(config["extra_math"])
		{
			seed: str = if calc == "bc" { "seed\n"; } else { "J\n"; };

			res2 := $ %(config_list["args"]) --seed=18923c.rlg << $seed;

			check_err_test(tgt_name, res2);
		}
	}

	test sym(path.join(testdir, "directory_as_file")): bin
	{
		testdir: str = path.dirname(tgt_name);

		res := $ %(config_list["args"]) $testdir;

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "binary_file")): bin
	{
		res := $ %(config_list["args"]) @(file_dep);

		check_err_test(tgt_name, res);
	}

	test sym(path.join(testdir, "binary_stdin")): bin
	{
		res := $ %(config_list["args"]) < @(file_dep);

		check_err_test(tgt_name, res);
	}
}

fn register_timeconst_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
) -> void
{
	timeconst: str = path.join(testdir, "scripts/timeconst.bc");

	if !path.isfile(path.join(src_dir, timeconst))
	{
		io.eprint("Warning: " +~ timeconst +~ " does not exist\n");
		io.eprint(timeconst +~ " is not part of this bc because of " +~
		          "license incompatibility\n");
		io.eprint("To test it, get it from the Linux kernel at " +~
		          "`kernel/time/timeconst.bc`\n");
		io.eprint("Skipping...\n");

		return;
	}

	for i: range(1001)
	{
		test sym(path.join(timeconst, str(i)))
		{
			idx: str = path.basename(tgt_name) +~ "\n";
			file: str = path.join(src_dir, path.dirname(tgt_name));

			// Generate.
			res1 := $ bc -q $file << $idx;

			if res1.exitcode != 0
			{
				io.eprint("Other bc is not GNU compatible. Skipping...\n");
				return;
			}

			// Run.
			res2 := $ %(config_list["args"]) -q $file << $idx;

			if res2.exitcode != 0 || res2.stdout != res1.stdout
			{
				error("\nFailed on input: " +~ idx +~ "\n");
			}
		}
	}
}

fn register_history_tests(
	bin: str,
	testdir: str,
	src_testdir: str,
) -> void
{
	calc: str = path.basename(testdir);

	src_test_scriptdir: str = path.dirname(src_testdir);

	len_res := $ @(path.join(src_test_scriptdir, "history.py")) $calc -a;

	if len_res.exitcode != 0
	{
		io.eprint("Python 3 with pexpect doesn't work. Skipping history tests");
		return;
	}

	len: usize = usize(str(len_res.stdout));

	for i: range(len)
	{
		test sym(calc +~ "/history/" +~ str(i)): bin
		{
			name: str = tgt_name;
			parts: []str = name.split("/");

			calc: str = parts[0];
			idx: str= parts[2];

			src_testdir: str = path.join(src_dir, "tests");

			$ @(path.join(src_testdir, "history.py")) -t $calc $idx @(file_dep);
		}
	}
}

/**
 * Generates all of the test targets for an executable.
 * @param name  The base name of the executable.
 * @param targets  The targets that tests should depend on.
 */
fn exe_tests(name: str) -> void
{
	bin: str = exe_name(name);

	testdir: str = path.join("tests", name);
	src_testdir: str = path.join(src_dir, testdir);

	halt: str = if name == "bc" { "halt"; } else { "q"; };
	gen_options: []str = if name == "bc" { @[ "-lq" ]; };
	options: []str = if name == "bc" { @[ "-lqc" ]; } else { @[ "-xc" ]; };

	other_num: str = "10000000000000000000000000000000000000000000000000" +~
	                 "0000000000000000000000000000";
	other_num70: str = "10000000000000000000000000000000000000000000000" +~
	                   "000000000000000000000\\\n0000000000";

	other_tests: []str =
	if name == "bc"
	{
		@[ "x", "extended-register", "line_length()", other_num ];
	}
	else
	{
		@[ "l", "mathlib", "glpR", other_num +~ "pR" ];
	};

	other_tests_results: []str = @[ other_num, other_num70 ];

	var: str = name.toupper() +~ "_LINE_LENGTH";

	script_options: []str =
	if name == "bc"
	{
		@[ "-lqC" ];
	}
	else
	{
		@[ "-xC" ];
	};

	error_options: []str = if name == "bc" { @[ "-ls" ]; } else { @[ "-x" ]; };

	args: []str =
	if bool(config["valgrind"])
	{
		VALGRIND_ARGS +~ @[ "./" +~ bin ];
	}
	else
	{
		@[ "./" +~ bin ];
	};

	test_config: Gaml = @(gaml){
		args: $args
		halt: $halt
		gen_options: $gen_options
		options: $options
		script_options: $script_options
		error_options: $error_options
		other_tests: $other_tests
		other_tests_results: $other_tests_results
		var: $var
	};

	push test_config: config_stack
	{
		extra: bool = bool(config["extra_math"]);

		register_standard_tests(bin, testdir, src_testdir, extra);
		register_script_tests(bin, testdir, src_testdir, extra);
		register_stdin_tests(bin, testdir, src_testdir);
		register_read_tests(bin, testdir, src_testdir);
		register_error_tests(bin, testdir, src_testdir);
		register_other_tests(bin, testdir, src_testdir, extra);

		if name == "bc" && bool(config["generated_tests"]) &&
		   path.isfile(path.join(src_testdir, "scripts/timeconst.bc"))
		{
			register_timeconst_tests(bin, testdir, src_testdir);
		}

		if host.os != "Windows" && sym(config["history"]) == @builtin
		{
			register_history_tests(bin, testdir, src_testdir);
		}
	}
}

/**
 * Gets the `$BINDIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$BINDIR`, with the `$DESTDIR`.
 */
fn get_bindir() -> str
{
	temp: str = str(config["bindir"]);

	bindir: str =
	if temp == ""
	{
		path.join(str(config["prefix"]), "bin");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, bindir);
}

/**
 * Gets the `$LIBDIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$LIBDIR`, with the `$DESTDIR`.
 */
fn get_libdir() -> str
{
	temp: str = str(config["libdir"]);

	libdir: str =
	if temp == ""
	{
		path.join(str(config["prefix"]), "lib");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, libdir);
}

/**
 * Gets the `$INCLUDEDIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$INCLUDEDIR`, with the `$DESTDIR`.
 */
fn get_includedir() -> str
{
	temp: str = str(config["includedir"]);

	includedir: str =
	if temp == ""
	{
		path.join(str(config["prefix"]), "include");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, includedir);
}

/**
 * Gets the `$PC_PATH`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$PC_PATH`, with the `$DESTDIR`.
 */
fn get_pc_path() -> str
{
	pc_path: str =
	if str(config["pc_path"]) == ""
	{
		res := $ pkg-config --variable=pc_path pkg-config;

		str(res.stdout);
	}
	else
	{
		str(config["pc_path"]);
	};

	return path.join(DESTDIR, pc_path);
}

/**
 * Gets the `$DATAROOTDIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$DATAROOTDIR`, with the `$DESTDIR`.
 */
fn get_datarootdir() -> str
{
	temp: str = str(config["datarootdir"]);

	datarootdir: str =
	if temp == ""
	{
		path.join(str(config["prefix"]), "share");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, datarootdir);
}

/**
 * Gets the `$DATADIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$DATADIR`, with the `$DESTDIR`.
 */
fn get_datadir() -> str
{
	temp: str = str(config["datadir"]);

	datadir: str =
	if temp == ""
	{
		get_datarootdir();
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, datadir);
}

/**
 * Gets the `$MANDIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$MANDIR`, with the `$DESTDIR`.
 */
fn get_mandir() -> str
{
	temp: str = str(config["mandir"]);

	mandir: str =
	if temp == ""
	{
		path.join(get_datadir(), "man");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, mandir);
}

/**
 * Gets the `$MAN1DIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$MAN1DIR`, with the `$DESTDIR`.
 */
fn get_man1dir() -> str
{
	temp: str = str(config["man1dir"]);

	man1dir: str =
	if temp == ""
	{
		path.join(get_mandir(), "man1");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, man1dir);
}

/**
 * Gets the `$MAN3DIR`, including the `$DESTDIR`. This generates the default
 * value if it wasn't set.
 * @return  The `$MAN3DIR`, with the `$DESTDIR`.
 */
fn get_man3dir() -> str
{
	temp: str = str(config["man3dir"]);

	man3dir: str =
	if temp == ""
	{
		path.join(get_mandir(), "man3");
	}
	else
	{
		temp;
	};

	return path.join(DESTDIR, man3dir);
}