xref: /freebsd/contrib/bc/build.pkg.rig (revision fdc4a7c8012b214986cfa2e2fb6d99731f004b1b)
1/*
2 * *****************************************************************************
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 *
6 * Copyright (c) 2018-2025 Gavin D. Howard and contributors.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
10 *
11 * * Redistributions of source code must retain the above copyright notice, this
12 *   list of conditions and the following disclaimer.
13 *
14 * * Redistributions in binary form must reproduce the above copyright notice,
15 *   this list of conditions and the following disclaimer in the documentation
16 *   and/or other materials provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
29 *
30 * *****************************************************************************
31 *
32 * The build package file.
33 *
34 */
35
36/// The path to the safe install script.
37SAFE_INSTALL: str = path.join(src_dir, "scripts/safe-install.sh");
38
39/// The file mode for executables, as an argument to the safe install script.
40EXEC_INSTALL_MODE: str = "-Dm755";
41
42/// The file mode for man pages and other files, as an argument to the safe
43/// install script.
44MANPAGE_INSTALL_MODE: str = "-Dm644";
45
46// Save this.
47OS: str = platform.os;
48
49DESTDIR: str = str(config["destdir"]);
50
51EXECPREFIX: str = str(config["execprefix"]);
52EXECSUFFIX: str = str(config["execsuffix"]);
53
54/**
55 * Generates the true executable name for the given base name.
56 * @param name  The base name of the executable.
57 * @return      The true name of the executable, including prefix, suffix, and
58                extension.
59 */
60fn exe_name(name: str) -> str
61{
62	temp: str = EXECPREFIX +~ name +~ EXECSUFFIX;
63	return if OS == "Windows" { temp +~ ".exe"; } else { temp; };
64}
65
66/**
67 * Generates the default executable name for the given base name.
68 * @param name  The base name of the executable.
69 * @return      The true name of the executable, including prefix, suffix, and
70                extension.
71 */
72fn default_exe_name(name: str) -> str
73{
74	return if OS == "Windows" { name +~ ".exe"; } else { name; };
75}
76
77/**
78 * Generates the true library name for the given base name.
79 * @param name  The base name of the library.
80 * @return      The true name of the library, including prefix and extension.
81 */
82fn lib_name(name: str) -> str
83{
84	ext: str = if OS == "Windows" { ".lib"; } else { ".a"; };
85	return "lib" +~ name +~ ext;
86}
87
88BC_BIN: str = exe_name("bc");
89DC_BIN: str = exe_name("dc");
90LIBRARY: str = lib_name("libbcl");
91
92BC_MANPAGE: str = EXECPREFIX +~ "bc" +~ EXECSUFFIX +~ ".1";
93DC_MANPAGE: str = EXECPREFIX +~ "dc" +~ EXECSUFFIX +~ ".1";
94BCL_MANPAGE: str = "bcl.3";
95
96BCL_HEADER: str = "bcl.h";
97BCL_HEADER_PATH: str = path.join(src_dir, path.join("include", BCL_HEADER));
98PC_FILE: str = "bcl.pc";
99
100/**
101 * Returns the string value of the define for a prompt default define for an
102 * executable.
103 * @param name  The base name of the executable.
104 * @return      The string value of the compiler define for the prompt default.
105 */
106fn prompt(name: str) -> str
107{
108	opt: sym = sym(config[name +~ "/default_prompt"]);
109
110	ret: str =
111	if opt == @off
112	{
113		"0";
114	}
115	else if opt == @tty_mode
116	{
117		str(uint(bool(config[name +~ "/default_tty_mode"])));
118	}
119	else
120	{
121		"1";
122	};
123
124	return ret;
125}
126
127HEADERS: []str = find_src_ext("include", "h");
128
129FORCE: bool = bool(config["force"]);
130
131BUILD_MODE: sym = sym(config["build_mode"]);
132
133BC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @bc));
134DC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @dc));
135LIBRARY_ENABLED: str = str(uint(BUILD_MODE == @library));
136
137EXTRA_MATH_ENABLED: str = str(uint(bool(config["extra_math"])));
138
139HISTORY: sym = sym(config["history"]);
140HISTORY_ENABLED: str = str(uint(HISTORY != @none));
141EDITLINE_ENABLED: str = str(uint(HISTORY == @editline));
142READLINE_ENABLED: str = str(uint(HISTORY == @readline));
143
144NLS_ENABLED: str =
145if OS == "Windows" || BUILD_MODE == @library
146{
147	"0";
148}
149else
150{
151	str(uint(sym(config["locales"]) != @none));
152};
153
154BUILD_TYPE: str =
155if EXTRA_MATH_ENABLED != "0" && HISTORY_ENABLED != "0" && NLS_ENABLED != "0"
156{
157	"A";
158}
159else
160{
161	t: str = if EXTRA_MATH_ENABLED != "0" { ""; } else { "E"; } +~
162	         if HISTORY_ENABLED != "0" { ""; } else { "H"; } +~
163	         if NLS_ENABLED != "0" { ""; } else { "N"; };
164
165	t;
166};
167
168OPTIMIZE: str = str(config["optimization"]);
169
170VALGRIND_ARGS: []str = @[
171	"valgrind",
172	"--error-exitcode=100",
173	"--leak-check=full",
174	"--show-leak-kinds=all",
175	"--errors-for-leak-kinds=all",
176	"--track-fds=yes",
177	"--track-origins=yes",
178];
179
180// Get the compiler. The user might have set one at the command line.
181CC: str = language.compiler;
182
183// Set optimization to "0" if it is empty.
184CFLAGS_OPT: str = if OPTIMIZE == "" { "0"; } else { OPTIMIZE; };
185
186// Get the command-line option for defining a preprocessor variable.
187DEFOPT: str = compiler_db["opt.define"];
188
189// Get the command-line string for the optimization option for the compiler.
190OPTOPT: str = compiler_db["opt.optimization"] +~ CFLAGS_OPT;
191
192// Get the compiler option for the object file to output to.
193OBJOUTOPT: str = compiler_db["opt.objout"];
194EXEOUTOPT: str = compiler_db["opt.exeout"];
195
196// Get the compiler option for outputting an object file rather than an
197// executable.
198OBJOPT: str = compiler_db["opt.obj"];
199
200// Get the compiler option for setting an include directory.
201INCOPT: str = compiler_db["opt.include"] +~ path.join(src_dir, "include");
202
203COVERAGE_CFLAGS: []str =
204if bool(config["coverage"])
205{
206	@[ "-fprofile-arcs", "-ftest-coverage", "-g", "-O0", DEFOPT +~ "NDEBUG" ];
207};
208
209MAINEXEC: str =
210if BUILD_MODE == @both || BUILD_MODE == @bc || BUILD_MODE == @library
211{
212	BC_BIN;
213}
214else
215{
216	DC_BIN;
217};
218
219MAINEXEC_FLAGS: []str = @[ DEFOPT +~ "MAINEXEC=" +~ MAINEXEC ];
220
221// XXX: Library needs these defines to be true.
222BC_DEF: str = if LIBRARY_ENABLED == "0" { BC_ENABLED; } else { "1"; };
223DC_DEF: str = if LIBRARY_ENABLED == "0" { DC_ENABLED; } else { "1"; };
224
225CFLAGS1: []str = config_list["cflags"] +~ @[ OPTOPT, INCOPT ] +~
226                 COVERAGE_CFLAGS +~ MAINEXEC_FLAGS;
227CFLAGS2: []str = @[
228	DEFOPT +~ "BC_ENABLED=" +~ BC_DEF,
229	DEFOPT +~ "DC_ENABLED=" +~ DC_DEF,
230	DEFOPT +~ "BUILD_TYPE=" +~ BUILD_TYPE,
231	DEFOPT +~ "EXECPREFIX=" +~ str(config["execprefix"]),
232	DEFOPT +~ "BC_NUM_KARATSUBA_LEN=" +~ str(num(config["karatsuba_len"])),
233	DEFOPT +~ "BC_ENABLE_LIBRARY=" +~ LIBRARY_ENABLED,
234	DEFOPT +~ "BC_ENABLE_NLS=" +~ NLS_ENABLED,
235	DEFOPT +~ "BC_ENABLE_EXTRA_MATH=" +~ EXTRA_MATH_ENABLED,
236	DEFOPT +~ "BC_ENABLE_HISTORY=" +~ HISTORY_ENABLED,
237	DEFOPT +~ "BC_ENABLE_EDITLINE=" +~ EDITLINE_ENABLED,
238	DEFOPT +~ "BC_ENABLE_READLINE=" +~ READLINE_ENABLED,
239	DEFOPT +~ "BC_ENABLE_MEMCHECK=" +~ str(uint(bool(config["memcheck"]))),
240	DEFOPT +~ "BC_ENABLE_AFL=" +~ str(uint(bool(config["afl"]))),
241	DEFOPT +~ "BC_ENABLE_OSSFUZZ=" +~ str(uint(bool(config["ossfuzz"]))),
242	DEFOPT +~ "BC_DEFAULT_BANNER=" +~
243	    str(uint(bool(config["bc/default_banner"]))),
244	DEFOPT +~ "BC_DEFAULT_SIGINT_RESET=" +~
245	    str(uint(bool(config["bc/default_sigint_reset"]))),
246	DEFOPT +~ "BC_DEFAULT_TTY_MODE=" +~
247	    str(uint(bool(config["bc/default_tty_mode"]))),
248	DEFOPT +~ "BC_DEFAULT_PROMPT=" +~ prompt("bc"),
249	DEFOPT +~ "BC_DEFAULT_EXPR_EXIT=" +~
250	    str(uint(bool(config["bc/default_expr_exit"]))),
251	DEFOPT +~ "BC_DEFAULT_DIGIT_CLAMP=" +~
252	    str(uint(bool(config["bc/default_digit_clamp"]))),
253	DEFOPT +~ "DC_DEFAULT_SIGINT_RESET=" +~
254	    str(uint(bool(config["dc/default_sigint_reset"]))),
255	DEFOPT +~ "DC_DEFAULT_TTY_MODE=" +~
256	    str(uint(bool(config["dc/default_tty_mode"]))),
257	DEFOPT +~ "DC_DEFAULT_PROMPT=" +~ prompt("dc"),
258	DEFOPT +~ "DC_DEFAULT_EXPR_EXIT=" +~
259	    str(uint(bool(config["dc/default_expr_exit"]))),
260	DEFOPT +~ "DC_DEFAULT_DIGIT_CLAMP=" +~
261	    str(uint(bool(config["dc/default_digit_clamp"]))),
262];
263CFLAGS: []str = CFLAGS1 +~ CFLAGS2;
264
265LDFLAGS: []str = config_list["ldflags"];
266
267COMMON_C_FILES: []str = @[
268	"src/data.c",
269	"src/num.c",
270	"src/rand.c",
271	"src/vector.c",
272	"src/vm.c",
273];
274
275EXEC_C_FILES: []str = @[
276	"src/args.c",
277	"src/file.c",
278	"src/lang.c",
279	"src/lex.c",
280	"src/main.c",
281	"src/opt.c",
282	"src/parse.c",
283	"src/program.c",
284	"src/read.c",
285];
286
287BC_C_FILES: []str = @[
288	"src/bc.c",
289	"src/bc_lex.c",
290	"src/bc_parse.c",
291];
292
293DC_C_FILES: []str = @[
294	"src/dc.c",
295	"src/dc_lex.c",
296	"src/dc_parse.c",
297];
298
299HISTORY_C_FILES: []str = @[
300	"src/history.c",
301];
302
303LIBRARY_C_FILES: []str = @[
304	"src/library.c",
305];
306
307GEN_HEADER1: str =
308    "// Copyright (c) 2018-2025 Gavin D. Howard and contributors.\n" +~
309    "// Licensed under the 2-clause BSD license.\n" +~
310    "// *** AUTOMATICALLY GENERATED FROM ";
311GEN_HEADER2: str = ". DO NOT MODIFY. ***\n\n";
312
313GEN_LABEL1: str = "const char *";
314GEN_LABEL2: str = " = \"";
315GEN_LABEL3: str = "\";\n\n";
316GEN_NAME1: str = "const char ";
317GEN_NAME2: str = "[] = {\n";
318
319GEN_LABEL_EXTERN1: str = "extern const char *";
320GEN_LABEL_EXTERN2: str = ";\n\n";
321GEN_NAME_EXTERN1: str = "extern const char ";
322GEN_NAME_EXTERN2: str = "[];\n\n";
323
324GEN_IFDEF1: str = "#if ";
325GEN_IFDEF2: str = "\n";
326GEN_ENDIF1: str = "#endif // ";
327GEN_ENDIF2: str = "\n";
328
329GEN_EX_START: str = "{{ A H N HN }}";
330GEN_EX_END: str = "{{ end }}";
331
332/// This is the max width to print characters to strgen files. This is to ensure
333/// that lines don't go much over 80 characters.
334MAX_WIDTH: usize = usize(72);
335
336/**
337 * A function to generate a C file that contains a C character array with the
338 * contents of a text file. For more detail, see the `gen/strgen.c` program;
339 * this function is exactly equivalent to that or should be.
340 * @param input        The input file name.
341 * @param output       The output file name.
342 * @param exclude      True if extra math stuff should be excluded, false if
343 *                     they should be included.
344 * @param name         The name of the array.
345 * @param label        If not equal to "", this is the label for the array,
346 *                     which is essentially the "file name" in `bc` and `dc`.
347 * @param define       If not equal to "", this is the preprocessor define
348 *                     expression that should be used to guard the array with a
349 *                     `#if`/`#endif` combo.
350 * @param remove_tabs  True if tabs should be ignored, false if they should be
351 *                     included.
352 */
353fn strgen(
354	input: str,
355	output: str,
356	exclude: bool,
357	name: str,
358	label: str,
359	def: str,
360	remove_tabs: bool,
361) -> void
362{
363	in: str = io.read_file(input);
364
365	io.open(output, "w"): f
366	{
367		f.print(GEN_HEADER1 +~ input +~ GEN_HEADER2);
368
369		if label != ""
370		{
371			f.print(GEN_LABEL_EXTERN1 +~ label +~ GEN_LABEL_EXTERN2);
372		}
373
374		f.print(GEN_NAME_EXTERN1 +~ name +~ GEN_NAME_EXTERN2);
375
376		if def != ""
377		{
378			f.print(GEN_IFDEF1 +~ def +~ GEN_IFDEF2);
379		}
380
381		if label != ""
382		{
383			f.print(GEN_LABEL1 +~ label +~ GEN_LABEL2 +~ name +~ GEN_LABEL3);
384		}
385
386		f.print(GEN_NAME1 +~ name +~ GEN_NAME2);
387
388		i: !usize = usize(0);
389		count: !usize = usize(0);
390		slashes: !usize = usize(0);
391
392		// This is where the end of the license comment is found.
393		while slashes < 2 && in[i] > 0
394		{
395			if slashes == 1 && in[i] == '*' && in[i + 1] == '/' &&
396			   (in[i + 2] == '\n' || in[i + 2] == '\r')
397			{
398				slashes! = slashes + usize(1);
399				i! = i + usize(2);
400			}
401			else if slashes == 0 && in[i] == '/' && in[i + 1] == '*'
402			{
403				slashes! = slashes + usize(1);
404				i! = i + usize(1);
405			}
406
407			i! = i + usize(1);
408		}
409
410		// The file is invalid if the end of the license comment could not be
411		// found.
412		if i == in.len
413		{
414			error("Could not find end of license comment");
415		}
416
417		i! = i + usize(1);
418
419		// Do not put extra newlines at the beginning of the char array.
420		while in[i] == '\n' || in[i] == '\r'
421		{
422			i! = i + usize(1);
423		}
424
425		// This loop is what generates the actual char array. It counts how many
426		// chars it has printed per line in order to insert newlines at
427		// appropriate places. It also skips tabs if they should be removed.
428		while i < in.len
429		{
430			if in[i] == '\r'
431			{
432				i! = i + usize(1);
433				continue;
434			}
435
436			// If we should output the character, i.e., it is not a tab or we
437			// can remove tabs...
438			if !remove_tabs || in[i] != '\t'
439			{
440				// Check for excluding something for extra math.
441				if in[i] == '{'
442				{
443					if i + GEN_EX_START.len <= in.len &&
444					   in.slice(i, i + GEN_EX_START.len) == GEN_EX_START
445					{
446						if exclude
447						{
448							// Get past the braces.
449							i! = i + usize(2);
450
451							// Find the end of the end.
452							while in[i] != '{' &&
453							      in.slice(i, i + GEN_EX_END.len) != GEN_EX_END
454							{
455								i! = i + usize(1);
456							}
457
458							i! = i + GEN_EX_END.len;
459
460							// Skip the last newline.
461							if in[i] == '\r'
462							{
463								i! = i + usize(1);
464							}
465
466							i! = i + usize(1);
467
468							continue;
469						}
470					}
471					else if !exclude &&
472					        in.slice(i, i + GEN_EX_END.len) == GEN_EX_END
473					{
474						i! = i + GEN_EX_END.len;
475
476						// Skip the last newline.
477						if in[i] == '\r'
478						{
479							i! = i + usize(1);
480						}
481
482						i! = i + usize(1);
483
484						continue;
485					}
486				}
487
488				// Print a tab if we are at the beginning of a line.
489				if count == 0
490				{
491					f.print("\t");
492				}
493
494				val: str = str(in[i]) +~ ",";
495
496				// Print the character.
497				f.print(val);
498
499				// Adjust the count.
500				count! = count + val.len;
501
502				if count > MAX_WIDTH
503				{
504					count! = usize(0);
505					f.print("\n");
506				}
507			}
508
509			i! = i + usize(1);
510		}
511
512		// Make sure the end looks nice.
513		if count == 0
514		{
515			f.print("  ");
516		}
517
518		// Insert the NUL byte at the end.
519		f.print("0\n};\n");
520
521		if def != ""
522		{
523			f.print(GEN_ENDIF1 +~ def +~ GEN_ENDIF2);
524		}
525	}
526}
527
528/**
529 * Creates a target to generate an object file from the given C file and returns
530 * the target name of the new target.
531 * @param c_file  The name of the C file target.
532 * @return        The name of the object file target.
533 */
534fn c2o(c_file: str) -> str
535{
536	o_file: str = c_file +~ (if OS == "Windows" { ".obj"; } else { ".o"; });
537
538	target o_file: c_file, HEADERS
539	{
540		$ $CC %(config_list["other_cflags"]) %(CFLAGS) $OBJOPT $OBJOUTOPT @(tgt)
541		  @(file_dep);
542	}
543
544	return o_file;
545}
546
547/**
548 * Generates a target to turn a text file into a C file with the text file's
549 * contents as a char array, then generates a target to generate an object file
550 * from that C file, then returns the name of the object file target.
551 * @param txt_file     The name of the text file.
552 * @param name         The name of the char array in the C file.
553 * @param label        The label for the array, if any. (See the @a strgen()
554 *                     function for more information.)
555 * @param def          The preprocessor define(s) to guard the array, if any.
556 *                     (See the @a strgen() function for more information.)
557 * @param remove_tabs  True if tabs should be ignored, false otherwise. (See the
558 *                     @a strgen() function for more information.)
559 * @return             The name of the object file target.
560 */
561fn txt2o(
562	txt_file: str,
563	name: str,
564	label: str,
565	def: str,
566	remove_tabs: bool,
567) -> str
568{
569	c_file: str = txt_file +~ ".c";
570
571	c_config: Gaml = @(gaml){
572		strgen_name: $name
573		strgen_label: $label
574		strgen_define: $def
575		strgen_remove_tabs: $remove_tabs
576	};
577
578	push c_config: config_stack
579	{
580		target c_file: txt_file
581		{
582			strgen(file_dep, tgt, EXTRA_MATH_ENABLED == "0",
583			       str(config["strgen_name"]), str(config["strgen_label"]),
584			       str(config["strgen_define"]),
585			       bool(config["strgen_remove_tabs"]));
586		}
587	}
588
589	return c2o(c_file);
590}
591
592/**
593 * Generates a target for an executable and returns its name.
594 * @param name     The name of the executable.
595 * @param o_files  The object files for the executable.
596 * @return         The name of the generated target.
597 */
598fn exe(name: str, o_files: []str) -> void
599{
600	target name: o_files
601	{
602		$ $CC %(config_list["other_cflags"]) %(config_list["strip_flag"])
603		  %(CFLAGS) %(LDFLAGS) $EXEOUTOPT @(tgt) %(file_deps);
604	}
605}
606
607/**
608 * Generates a target for a link.
609 * @param name  The name of the link.
610 * @param exec  The name of the executable target.
611 */
612fn ln(name: str, exec: str) -> void
613{
614	if OS == "Windows"
615	{
616		target name: exec
617		{
618			$ copy /v /y /b @(file_dep) @(tgt);
619		}
620	}
621	else
622	{
623		target name: exec
624		{
625			$ ln -fs @("./" +~ path.basename(file_dep)) @(tgt);
626		}
627	}
628}
629
630/**
631 * Generates a target for a library.
632 * @param name  The name of the library.
633 * @param exec  The name of the executable target.
634 */
635fn lib(name: str, o_files: []str) -> void
636{
637	if OS == "WINDOWS"
638	{
639		exe(name, o_files);
640	}
641	else
642	{
643		target name: o_files
644		{
645			$ ar -r -cu @(tgt) %(file_deps);
646		}
647	}
648}
649
650fn check_err_test(
651	name: str,
652	res: CmdResult,
653) -> void
654{
655	if res.exitcode > 127
656	{
657		error("Test \"" +~ name +~ "\" crashed");
658	}
659
660	if res.exitcode == 0
661	{
662		error("Test \"" +~ name +~ "\" returned no error");
663	}
664
665	if res.exitcode == 100
666	{
667		error("Test \"" +~ name +~ "\" had memory errors on non-fatal error\n");
668	}
669
670	if res.stderr.len <= 1
671	{
672		error("Test \"" +~ name +~ "\" produced no error message");
673	}
674}
675
676fn check_test_retcode(
677	name: str,
678	exitcode: uint,
679) -> void
680{
681	if exitcode != 0
682	{
683		error("Test \"" +~ name +~ "\" failed with exitcode: " +~
684		      str(exitcode) +~ "\n");
685	}
686}
687
688fn check_test(
689	name: str,
690	res: CmdResult,
691	exp_path: str,
692) -> void
693{
694	check_test_retcode(name, res.exitcode);
695
696	exp := io.read_file_bytes(exp_path);
697
698	if exp != res.stdout_full
699	{
700		error("Test \"" +~ name +~ "\" failed\n" +~ str(res.stderr));
701	}
702}
703
704fn register_standard_tests(
705	bin: str,
706	testdir: str,
707	src_testdir: str,
708	extra: bool,
709) -> void
710{
711	all_file: str = path.join(src_testdir, "all.txt");
712	tests: []str = io.read_file(all_file).split("\n");
713
714	extra_path := path.join(src_dir, "tests/extra_required.txt");
715	extra_required: []str = io.read_file(extra_path).split("\n");
716
717	for t: tests
718	{
719		if t == ""
720		{
721			continue;
722		}
723
724		// Skip extra math tests if it is not enabled.
725		if !extra && extra_required contains t
726		{
727			continue;
728		}
729
730		test sym(path.join(testdir, t)): bin
731		{
732			halt: str = str(config["halt"]);
733
734			name: str = path.basename(tgt_name);
735			testdir: str = path.dirname(tgt_name);
736			calc: str = path.basename(testdir);
737
738			test_file: str = tgt_name +~ ".txt";
739			test_result_file: str = tgt_name +~ "_results.txt";
740
741			src_test_file: str = path.join(src_dir, test_file);
742			src_test_result_file: str = path.join(src_dir, test_result_file);
743
744			actual_test_file: str =
745			if !path.isfile(src_test_file)
746			{
747				// If we shouldn't generate tests, skip.
748				if !bool(config["generated_tests"])
749				{
750					io.eprint("Skipping test " +~ tgt_name +~ "\n");
751					return;
752				}
753
754				script_name: str = name +~ "." +~ calc;
755
756				scriptdir: str = path.join(testdir, "scripts");
757				src_scriptdir: str = path.join(src_dir, scriptdir);
758				src_script_name: str = path.join(src_scriptdir, script_name);
759
760				$ @(default_exe_name(calc)) $src_script_name > $test_file;
761
762				test_file;
763			}
764			else
765			{
766				src_test_file;
767			};
768
769			exp_result_file: str =
770			if !path.isfile(src_test_result_file)
771			{
772				tmpfile: str = path.tmp(calc +~ "_test_result");
773
774				$ @(default_exe_name(calc)) %(config_list["gen_options"])
775				  $actual_test_file << $halt > $tmpfile;
776
777				tmpfile;
778			}
779			else
780			{
781				src_test_result_file;
782			};
783
784			res := $ %(config_list["args"]) %(config_list["options"])
785			         $actual_test_file << $halt;
786
787			check_test(tgt_name, res, exp_result_file);
788		}
789	}
790}
791
792fn register_script_tests(
793	bin: str,
794	testdir: str,
795	src_testdir: str,
796	extra: bool,
797) -> void
798{
799	scriptdir: str = path.join(testdir, "scripts");
800	src_scriptdir: str = path.join(src_testdir, "scripts");
801	all_file: str = path.join(src_scriptdir, "all.txt");
802	tests: []str = io.read_file(all_file).split("\n");
803
804	for t: tests
805	{
806		if t == ""
807		{
808			continue;
809		}
810
811		// Skip extra math tests if it is not enabled.
812		if !extra && (t == "rand.bc" || t == "root.bc" || t == "i2rand.bc")
813		{
814			continue;
815		}
816
817		test sym(path.join(scriptdir, t)): bin
818		{
819			halt: str = str(config["halt"]);
820
821			name: str = path.basename(tgt_name);
822			testdir: str = path.dirname(tgt_name);
823			testdir2: str = path.dirname(testdir);
824			calc: str = path.basename(testdir2);
825
826			test_file: str = tgt_name;
827			test_file_dir: str = path.dirname(tgt_name);
828			test_file_name: str = path.basename(tgt_name, "." +~ calc);
829			test_result_file: str = path.join(test_file_dir,
830			                                  test_file_name +~ ".txt");
831
832			src_test_file: str = path.join(src_dir, test_file);
833			src_test_result_file: str = path.join(src_dir, test_result_file);
834
835			exp_result_file: str =
836			if !path.isfile(src_test_result_file)
837			{
838				tmpfile: str = path.tmp(calc +~ "_script_test_result");
839
840				// This particular test needs to be generated straight. Also, on
841				// Windows, we don't have `sed`, and the `bc`/`dc` there is
842				// probably this one anyway.
843				if name == "stream.dc" || host.os == "Windows"
844				{
845					$ @(default_exe_name(calc)) $src_test_file << $halt
846					  > $tmpfile;
847				}
848				else
849				{
850					root_testdir: str = path.join(src_dir, "tests");
851
852					// This sed and the script are to remove an incompatibility
853					// with GNU bc, where GNU bc is wrong. See the development
854					// manual (manuals/development.md#script-tests) for more
855					// information.
856					$ @(default_exe_name(calc)) $src_test_file << $halt |
857					  sed -n -f @(path.join(root_testdir, "script.sed"))
858					  > $tmpfile;
859				}
860
861				tmpfile;
862			}
863			else
864			{
865				src_test_result_file;
866			};
867
868			if calc == "bc"
869			{
870				res1 := $ %(config_list["args"]) -g
871				          %(config_list["script_options"]) $src_test_file
872				          << $halt;
873
874				check_test(tgt_name, res1, exp_result_file);
875			}
876
877			// These tests do not need to run without global stacks.
878			if name == "globals.bc" || name == "references.bc" ||
879			   name == "rand.bc"
880			{
881				return;
882			}
883
884			res2 := $ %(config_list["args"]) %(config_list["script_options"])
885			          $src_test_file << $halt;
886
887			check_test(tgt_name, res2, exp_result_file);
888		}
889	}
890}
891
892fn register_stdin_test(
893	bin: str,
894	testdir: str,
895	name: str
896) -> void
897{
898	test sym(path.join(testdir, name)): bin
899	{
900		name: str = path.basename(tgt_name);
901		testdir: str = path.dirname(tgt_name);
902		calc: str = path.basename(testdir);
903
904		halt: str = if name == "bc" { "halt"; } else { "q"; };
905
906		test_file: str = tgt_name +~ ".txt";
907		test_result_file: str = tgt_name +~ "_results.txt";
908
909		src_test_file: str = path.join(src_dir, test_file);
910		src_test_result_file: str = path.join(src_dir, test_result_file);
911
912		res := $ %(config_list["args"]) %(config_list["options"])
913		         < $src_test_file;
914
915		check_test(tgt_name, res, src_test_result_file);
916	}
917}
918
919fn register_stdin_tests(
920	bin: str,
921	testdir: str,
922	src_testdir: str,
923) -> void
924{
925	calc: str = path.basename(testdir);
926
927	if calc == "bc"
928	{
929		for t: @[ "stdin", "stdin1", "stdin2" ]
930		{
931			register_stdin_test(bin, testdir, t);
932		}
933	}
934	else
935	{
936		// dc only needs one.
937		register_stdin_test(bin, testdir, "stdin");
938	}
939}
940
941fn register_read_tests(
942	bin: str,
943	testdir: str,
944	src_testdir: str,
945) -> void
946{
947	calc: str = path.basename(testdir);
948
949	read_call: str = if calc == "bc" { "read()"; } else { "?"; };
950	read_expr: str =
951	if calc == "bc"
952	{
953		read_call +~ "\n5+5;";
954	}
955	else
956	{
957		read_call;
958	};
959	read_multiple: str =
960	if calc == "bc"
961	{
962		"3\n2\n1\n";
963	}
964	else
965	{
966		"3pR\n2pR\n1pR\n";
967	};
968
969	read_test_config: Gaml = @(gaml){
970		read_call: $read_call
971		read_expr: $read_expr
972		read_multiple: $read_multiple
973	};
974
975	push read_test_config: config_stack
976	{
977		// First test is the regular read test.
978		test sym(path.join(testdir, "read")): bin
979		{
980			testdir: str = path.dirname(tgt_name);
981			src_testdir: str = path.join(src_dir, testdir);
982
983			test_file: str = tgt_name +~ ".txt";
984			src_test_file: str = path.join(src_dir, test_file);
985
986			read_call: str = str(config["read_call"]);
987
988			lines: []str = io.read_file(src_test_file).split("\n");
989
990			for l: lines
991			{
992				if l == ""
993				{
994					continue;
995				}
996
997				res := $ %(config_list["args"]) %(config_list["options"])
998				         << @(read_call +~ "\n" +~ l +~ "\n");
999
1000				check_test(tgt_name, res,
1001				           path.join(src_testdir, "read_results.txt"));
1002			}
1003		}
1004
1005		// Next test is reading multiple times.
1006		test sym(path.join(testdir, "read_multiple")): bin
1007		{
1008			testdir: str = path.dirname(tgt_name);
1009
1010			test_file: str = tgt_name +~ ".txt";
1011
1012			path.mkdirp(path.dirname(test_file));
1013
1014			read_call: str = str(config["read_call"]);
1015
1016			exp_path: str = path.tmp("read_multiple_results");
1017
1018			io.open(exp_path, "w"): f
1019			{
1020				f.print("3\n2\n1\n");
1021			}
1022
1023			res := $ %(config_list["args"]) %(config_list["options"])
1024			         -e $read_call -e $read_call -e $read_call
1025			         << @(str(config["read_multiple"]));
1026
1027			check_test(tgt_name, res, exp_path);
1028		}
1029
1030		// Next test is the read errors test.
1031		test sym(path.join(testdir, "read_errors")): bin
1032		{
1033			testdir: str = path.dirname(tgt_name);
1034			src_testdir: str = path.join(src_dir, testdir);
1035
1036			test_file: str = tgt_name +~ ".txt";
1037			src_test_file: str = path.join(src_dir, test_file);
1038
1039			path.mkdirp(path.dirname(test_file));
1040
1041			read_call: str = str(config["read_call"]);
1042
1043			lines: []str = io.read_file(src_test_file).split("\n");
1044
1045			for l: lines
1046			{
1047				if l == ""
1048				{
1049					continue;
1050				}
1051
1052				res := $ %(config_list["args"]) %(config_list["options"])
1053				         << @(read_call +~ "\n" +~ l +~ "\n");
1054
1055				check_err_test(tgt_name, res);
1056			}
1057		}
1058
1059		// Next test is the empty read test.
1060		test sym(path.join(testdir, "read_empty")): bin
1061		{
1062			read_call: str = str(config["read_call"]);
1063
1064			res := $ %(config_list["args"]) %(config_list["options"])
1065			         << @(read_call +~ "\n");
1066
1067			check_err_test(tgt_name, res);
1068		}
1069
1070		// Next test is the read EOF test.
1071		test sym(path.join(testdir, "read_EOF")): bin
1072		{
1073			read_call: str = str(config["read_call"]);
1074
1075			res := $ %(config_list["args"]) %(config_list["options"])
1076			         << $read_call;
1077
1078			check_err_test(tgt_name, res);
1079		}
1080	}
1081}
1082
1083fn run_error_lines_test(name: str) -> void
1084{
1085	file: str = path.join(src_dir, name);
1086
1087	lines: []str = io.read_file(file).split("\n");
1088
1089	for l: lines
1090	{
1091		if l == ""
1092		{
1093			continue;
1094		}
1095
1096		res := $ %(config_list["args"]) %(config_list["options"])
1097		         %(config_list["error_options"]) << @(l +~ "\n");
1098
1099		check_err_test(name, res);
1100	}
1101}
1102
1103fn register_error_tests(
1104	bin: str,
1105	testdir: str,
1106	src_testdir: str,
1107) -> void
1108{
1109	calc: str = path.basename(testdir);
1110
1111	// First test is command-line expression error.
1112	test sym(path.join(testdir, "command-line_expr_error")): bin
1113	{
1114		halt: str = str(config["halt"]);
1115
1116		res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
1117		         -e "2+2" << $halt;
1118
1119		check_err_test(tgt_name, res);
1120	}
1121
1122	// First test is command-line file expression error.
1123	test sym(path.join(testdir, "command-line_file_expr_error")): bin
1124	{
1125		testdir: str = path.dirname(tgt_name);
1126		halt: str = str(config["halt"]);
1127
1128		res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
1129		         -f @(path.join(testdir, "decimal.txt")) << $halt;
1130
1131		check_err_test(tgt_name, res);
1132	}
1133
1134	if calc == "bc"
1135	{
1136		test sym(path.join(testdir, "posix_warning")): bin
1137		{
1138			res := $ %(config_list["args"]) %(config_list["options"]) -w
1139			         << @("line");
1140
1141			if res.exitcode != 0
1142			{
1143				error("Test \"" +~ tgt_name +~ "\" returned an error (" +~
1144				      str(res.exitcode) +~ ")");
1145			}
1146
1147			output: str = str(res.stderr);
1148
1149			if output == "" || output == "\n"
1150			{
1151				error("Test \"" +~ tgt_name +~ "\" did not print a warning");
1152			}
1153		}
1154
1155		test sym(path.join(testdir, "posix_errors.txt")): bin
1156		{
1157			run_error_lines_test(tgt_name);
1158		}
1159	}
1160
1161	test sym(path.join(testdir, "errors.txt")): bin
1162	{
1163		run_error_lines_test(tgt_name);
1164	}
1165
1166	errors_dir: str = path.join(testdir, "errors");
1167
1168	for f: find_src_ext(errors_dir, "txt")
1169	{
1170		// Skip the problematic test, if requested.
1171		if calc == "bc" && f contains "33.txt" &&
1172		   !bool(config["problematic_tests"])
1173		{
1174			continue;
1175		}
1176
1177		test sym(f): bin
1178		{
1179			errors_dir: str = path.dirname(tgt_name);
1180			testdir: str = path.dirname(errors_dir);
1181			calc: str = path.basename(testdir);
1182
1183			halt: str = str(config["halt"]);
1184
1185			res1 := $ %(config_list["args"]) %(config_list["error_options"]) -c
1186			          @(tgt_name) << $halt;
1187
1188			check_err_test(tgt_name, res1);
1189
1190			res2 := $ %(config_list["args"]) %(config_list["error_options"]) -C
1191			          @(tgt_name) << $halt;
1192
1193			check_err_test(tgt_name, res2);
1194
1195			res3 := $ %(config_list["args"]) %(config_list["error_options"]) -c
1196			          < @(path.join(src_dir, tgt_name));
1197
1198			check_err_test(tgt_name, res3);
1199
1200			res4 := $ %(config_list["args"]) %(config_list["error_options"]) -C
1201			          < @(path.join(src_dir, tgt_name));
1202
1203			check_err_test(tgt_name, res4);
1204		}
1205	}
1206}
1207
1208fn check_kwredef_test(
1209	name: str,
1210	res: CmdResult,
1211) -> void
1212{
1213	testdir: str = path.dirname(name);
1214	redefine_exp: str = path.join(testdir, "redefine_exp.txt");
1215
1216	check_test(tgt_name, res, redefine_exp);
1217}
1218
1219OTHER_LINE_LEN_RESULTS_NAME: str = "line_length_test_results.txt";
1220OTHER_LINE_LEN70_RESULTS_NAME: str = "line_length70_test_results.txt";
1221OTHER_MATHLIB_SCALE_RESULTS_NAME: str = "mathlib_scale_results.txt";
1222
1223fn register_other_tests(
1224	bin: str,
1225	testdir: str,
1226	src_testdir: str,
1227	extra: bool,
1228) -> void
1229{
1230	calc: str = path.basename(testdir);
1231
1232	path.mkdirp(testdir);
1233
1234	// Halt test.
1235	test sym(path.join(testdir, "halt")): bin
1236	{
1237		halt: str = str(config["halt"]) +~ "\n";
1238
1239		res := $ %(config_list["args"]) << $halt;
1240
1241		check_test_retcode(tgt_name, res.exitcode);
1242	}
1243
1244	if calc == "bc"
1245	{
1246		// bc has two halt or quit commands, so test the second as well.
1247		test sym(path.join(testdir, "quit")): bin
1248		{
1249			res := $ %(config_list["args"]) << @("quit\n");
1250
1251			check_test_retcode(tgt_name, res.exitcode);
1252		}
1253
1254		// Also, make sure quit only quits after an expression.
1255		test sym(path.join(testdir, "quit_after_expr")): bin
1256		{
1257			res := $ %(config_list["args"]) -e "1+1" << @("quit\n");
1258
1259			check_test_retcode(tgt_name, res.exitcode);
1260
1261			if str(res.stdout) != "2"
1262			{
1263				error("Test \"" +~ tgt_name +~
1264				      "\" did not have the right output");
1265			}
1266		}
1267
1268		test sym(path.join(testdir, "env_args1")): bin
1269		{
1270			env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
1271			{
1272				res := $ %(config_list["args"]) << @("s(.02893)\n");
1273
1274				check_test_retcode(tgt_name, res.exitcode);
1275			}
1276		}
1277
1278		test sym(path.join(testdir, "env_args2")): bin
1279		{
1280			env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
1281			{
1282				res := $ %(config_list["args"]) -e 4 << @("halt\n");
1283
1284				check_test_retcode(tgt_name, res.exitcode);
1285			}
1286		}
1287
1288		redefine_exp: str = path.join(testdir, "redefine_exp.txt");
1289
1290		io.open(redefine_exp, "w"): f
1291		{
1292			f.print("5\n0\n");
1293		}
1294
1295		test sym(path.join(testdir, "keyword_redefinition1")): bin
1296		{
1297			res := $ %(config_list["args"]) --redefine=print -e
1298			         "define print(x) { x }" -e "print(5)" << @("halt\n");
1299
1300			check_kwredef_test(tgt_name, res);
1301		}
1302
1303		test sym(path.join(testdir, "keyword_redefinition2")): bin
1304		{
1305			res := $ %(config_list["args"]) -r abs -r else -e
1306			         "abs = 5; else = 0" -e "abs;else" << @("halt\n");
1307
1308			check_kwredef_test(tgt_name, res);
1309		}
1310
1311		if extra
1312		{
1313			test sym(path.join(testdir, "keyword_redefinition_lib2")): bin
1314			{
1315				res := $ %(config_list["args"]) -lr abs -e "perm(5, 1)" -e 0
1316				         << @("halt\n");
1317
1318				check_kwredef_test(tgt_name, res);
1319			}
1320
1321			test sym(path.join(testdir, "leading_zero_script")): bin
1322			{
1323				testdir: str = path.dirname(tgt_name);
1324				src_testdir: str = path.join(src_dir, testdir);
1325
1326				res := $ %(config_list["args"]) -lz
1327				         @(path.join(src_testdir, "leadingzero.txt"))
1328				         << @(str(config["halt"]));
1329
1330				check_test(tgt_name, res,
1331				           path.join(src_testdir, "leadingzero_results.txt"));
1332			}
1333		}
1334
1335		test sym(path.join(testdir, "keyword_redefinition3")): bin
1336		{
1337			res := $ %(config_list["args"]) -r abs -r else -e
1338			         "abs = 5; else = 0" -e "abs;else" << @("halt\n");
1339
1340			check_kwredef_test(tgt_name, res);
1341		}
1342
1343		test sym(path.join(testdir, "keyword_redefinition_error")): bin
1344		{
1345			res := $ %(config_list["args"]) -r break -e "define break(x) { x }";
1346
1347			check_err_test(tgt_name, res);
1348		}
1349
1350		test sym(path.join(testdir,
1351		                   "keyword_redefinition_without_redefine")): bin
1352		{
1353			res := $ %(config_list["args"]) -e "define read(x) { x }";
1354
1355			check_err_test(tgt_name, res);
1356		}
1357
1358		test sym(path.join(testdir, "multiline_comment_in_expr_file")): bin
1359		{
1360			testdir: str = path.dirname(tgt_name);
1361			src_testdir: str = path.join(src_dir, testdir);
1362
1363			// tests/bc/misc1.txt happens to have a multiline comment in it.
1364			src_test_file: str = path.join(src_testdir, "misc1.txt");
1365			src_test_results_file: str = path.join(src_testdir,
1366			                                       "misc1_results.txt");
1367
1368			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
1369
1370			check_test(tgt_name, res, src_test_results_file);
1371		}
1372
1373		test sym(path.join(testdir,
1374		                   "multiline_comment_error_in_expr_file")): bin
1375		{
1376			testdir: str = path.dirname(tgt_name);
1377			src_testdir: str = path.join(src_dir, testdir);
1378
1379			src_test_file: str = path.join(src_testdir, "errors/05.txt");
1380
1381			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
1382
1383			check_err_test(tgt_name, res);
1384		}
1385
1386		test sym(path.join(testdir, "multiline_string_in_expr_file")): bin
1387		{
1388			testdir: str = path.dirname(tgt_name);
1389			src_testdir: str = path.join(src_dir, testdir);
1390
1391			// tests/bc/strings.txt happens to have a multiline string in it.
1392			src_test_file: str = path.join(src_testdir, "strings.txt");
1393			src_test_results_file: str = path.join(src_testdir,
1394			                                       "strings_results.txt");
1395
1396			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
1397
1398			check_test(tgt_name, res, src_test_results_file);
1399		}
1400
1401		tst := path.join(testdir,
1402		                 "multiline_string_with_backslash_error_in_expr_file");
1403
1404		test sym(tst): bin
1405		{
1406			testdir: str = path.dirname(tgt_name);
1407			src_testdir: str = path.join(src_dir, testdir);
1408
1409			src_test_file: str = path.join(src_testdir, "errors/16.txt");
1410
1411			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
1412
1413			check_err_test(tgt_name, res);
1414		}
1415
1416		tst2 := path.join(testdir, "multiline_string_error_in_expr_file");
1417
1418		test sym(tst2): bin
1419		{
1420			testdir: str = path.dirname(tgt_name);
1421			src_testdir: str = path.join(src_dir, testdir);
1422
1423			src_test_file: str = path.join(src_testdir, "errors/04.txt");
1424
1425			res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
1426
1427			check_err_test(tgt_name, res);
1428		}
1429
1430		test sym(path.join(testdir, "interactive_halt")): bin
1431		{
1432			res := $ %(config_list["args"]) -i << @("halt\n");
1433
1434			check_test_retcode(tgt_name, res.exitcode);
1435		}
1436	}
1437	else
1438	{
1439		test sym(path.join(testdir, "env_args1")): bin
1440		{
1441			env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
1442			{
1443				res := $ %(config_list["args"]) << @("4s stuff\n");
1444
1445				check_test_retcode(tgt_name, res.exitcode);
1446			}
1447		}
1448
1449		test sym(path.join(testdir, "env_args2")): bin
1450		{
1451			env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
1452			{
1453				res := $ %(config_list["args"]) -e 4pR;
1454
1455				check_test_retcode(tgt_name, res.exitcode);
1456			}
1457		}
1458
1459		test sym(path.join(testdir, "extended_register_command1")): bin
1460		{
1461			testdir: str = path.dirname(tgt_name);
1462			results: str = tgt_name +~ ".txt";
1463
1464			path.mkdirp(testdir);
1465
1466			io.open(results, "w"): f
1467			{
1468				f.print("0\n");
1469			}
1470
1471			res := $ %(config_list["args"]) -e gxpR << @("q\n");
1472
1473			check_test(tgt_name, res, results);
1474		}
1475
1476		test sym(path.join(testdir, "extended_register_command2")): bin
1477		{
1478			testdir: str = path.dirname(tgt_name);
1479			results: str = tgt_name +~ ".txt";
1480
1481			path.mkdirp(testdir);
1482
1483			io.open(results, "w"): f
1484			{
1485				f.print("1\n");
1486			}
1487
1488			res := $ %(config_list["args"]) -x -e gxpR << @("q\n");
1489
1490			check_test(tgt_name, res, results);
1491		}
1492	}
1493
1494	path.mkdirp(testdir);
1495
1496	other_tests_results: []str = config_list["other_tests_results"];
1497
1498	io.open(path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME), "w"): f
1499	{
1500		f.print(other_tests_results[0] +~ "\n");
1501	}
1502
1503	io.open(path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME), "w"): f
1504	{
1505		f.print(other_tests_results[1] +~ "\n");
1506	}
1507
1508	test sym(path.join(testdir, "line_length1")): bin
1509	{
1510		env.set env.str(str(config["var"]), "80")
1511		{
1512			testdir: str = path.dirname(tgt_name);
1513
1514			other_tests: []str = config_list["other_tests"];
1515
1516			res := $ %(config_list["args"]) << @(other_tests[3]);
1517
1518			check_test(tgt_name, res,
1519			           path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
1520		}
1521	}
1522
1523	test sym(path.join(testdir, "line_length2")): bin
1524	{
1525		env.set env.str(str(config["var"]), "2147483647")
1526		{
1527			testdir: str = path.dirname(tgt_name);
1528
1529			other_tests: []str = config_list["other_tests"];
1530
1531			res := $ %(config_list["args"]) << @(other_tests[3]);
1532
1533			check_test(tgt_name, res,
1534			           path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME));
1535		}
1536	}
1537
1538	test sym(path.join(testdir, "expr_and_file_args_test")): bin
1539	{
1540		testdir: str = path.dirname(tgt_name);
1541		src_testdir: str = path.join(src_dir, testdir);
1542
1543		input_file: str = path.join(src_testdir, "add.txt");
1544		input: str = io.read_file(input_file);
1545		results_file: str = path.join(src_testdir, "add_results.txt");
1546		results: str = io.read_file(results_file);
1547
1548		output_file: str = path.join(testdir, "expr_file_args.txt");
1549
1550		io.open(output_file, "w"): f
1551		{
1552			f.print(results +~ results +~ results +~ results);
1553		}
1554
1555		res := $ %(config_list["args"]) -e $input -f $input_file
1556		         --expression $input --file $input_file
1557		         -e @(str(config["halt"]));
1558
1559		check_test(tgt_name, res, output_file);
1560	}
1561
1562	test sym(path.join(testdir, "files_test")): bin
1563	{
1564		env.set env.str(str(config["var"]), "2147483647")
1565		{
1566			testdir: str = path.dirname(tgt_name);
1567			src_testdir: str = path.join(src_dir, testdir);
1568
1569			input_file: str = path.join(src_testdir, "add.txt");
1570			input: str = io.read_file(input_file);
1571			results_file: str = path.join(src_testdir, "add_results.txt");
1572			results: str = io.read_file(results_file);
1573
1574			output_file: str = path.join(testdir, "files.txt");
1575
1576			io.open(output_file, "w"): f
1577			{
1578				f.print(results +~ results +~ results +~ results);
1579			}
1580
1581			res := $ %(config_list["args"]) -- $input_file $input_file
1582			         $input_file $input_file << @(str(config["halt"]));
1583
1584			check_test(tgt_name, res, output_file);
1585		}
1586	}
1587
1588	test sym(path.join(testdir, "line_length3")): bin
1589	{
1590		env.set env.str(str(config["var"]), "62")
1591		{
1592			testdir: str = path.dirname(tgt_name);
1593
1594			other_tests: []str = config_list["other_tests"];
1595
1596			res := $ %(config_list["args"]) -L << @(other_tests[3]);
1597
1598			check_test(tgt_name, res,
1599			           path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
1600		}
1601	}
1602
1603	test sym(path.join(testdir, "line_length_func")): bin
1604	{
1605		env.set env.str(str(config["var"]), "62")
1606		{
1607			testdir: str = path.dirname(tgt_name);
1608			results: str = tgt_name +~ ".txt";
1609
1610			path.mkdirp(testdir);
1611
1612			io.open(results, "w"): f
1613			{
1614				f.print("0\n");
1615			}
1616
1617			other_tests: []str = config_list["other_tests"];
1618
1619			res := $ %(config_list["args"]) -L << @(other_tests[2]);
1620
1621			check_test(tgt_name, res, results);
1622		}
1623	}
1624
1625	test sym(path.join(testdir, "arg")): bin
1626	{
1627		halt: str = str(config["halt"]);
1628
1629		res1 := $ %(config_list["args"]) -h << $halt;
1630		check_test_retcode(tgt_name, res1.exitcode);
1631
1632		res2 := $ %(config_list["args"]) -P << $halt;
1633		check_test_retcode(tgt_name, res2.exitcode);
1634
1635		res3 := $ %(config_list["args"]) -R << $halt;
1636		check_test_retcode(tgt_name, res3.exitcode);
1637
1638		res4 := $ %(config_list["args"]) -v << $halt;
1639		check_test_retcode(tgt_name, res4.exitcode);
1640
1641		res5 := $ %(config_list["args"]) -V << $halt;
1642		check_test_retcode(tgt_name, res5.exitcode);
1643	}
1644
1645	test sym(path.join(testdir, "leading_zero_arg")): bin
1646	{
1647		testdir: str = path.dirname(tgt_name);
1648		calc: str = path.basename(testdir);
1649
1650		expected_file: str = tgt_name +~ ".txt";
1651
1652		expected: str = "0.1\n-0.1\n1.1\n-1.1\n0.1\n-0.1\n";
1653
1654		io.open(expected_file, "w"): f
1655		{
1656			f.print(expected);
1657		}
1658
1659		data: str =
1660		if calc == "bc"
1661		{
1662			"0.1\n-0.1\n1.1\n-1.1\n.1\n-.1\n";
1663		}
1664		else
1665		{
1666			"0.1pR\n_0.1pR\n1.1pR\n_1.1pR\n.1pR\n_.1pR\n";
1667		};
1668
1669		res := $ %(config_list["args"]) -z << $data;
1670
1671		check_test(tgt_name, res, expected_file);
1672	}
1673
1674	test sym(path.join(testdir, "invalid_file_arg")): bin
1675	{
1676		res := $ %(config_list["args"]) -f
1677		         "astoheusanotehynstahonsetihaotsnuhynstahoaoetusha.txt";
1678
1679		check_err_test(tgt_name, res);
1680	}
1681
1682	test sym(path.join(testdir, "invalid_option_arg")): bin
1683	{
1684		other_tests: []str = config_list["other_tests"];
1685
1686		res := $ %(config_list["args"]) @("-" +~ other_tests[0])
1687		         -e @(str(config["halt"]));
1688
1689		check_err_test(tgt_name, res);
1690	}
1691
1692	test sym(path.join(testdir, "invalid_long_option_arg")): bin
1693	{
1694		other_tests: []str = config_list["other_tests"];
1695
1696		res := $ %(config_list["args"]) @("--" +~ other_tests[1])
1697		         -e @(str(config["halt"]));
1698
1699		check_err_test(tgt_name, res);
1700	}
1701
1702	test sym(path.join(testdir, "unrecognized_option_arg")): bin
1703	{
1704		res := $ %(config_list["args"]) -u -e @(str(config["halt"]));
1705
1706		check_err_test(tgt_name, res);
1707	}
1708
1709	test sym(path.join(testdir, "unrecognized_long_option_arg")): bin
1710	{
1711		res := $ %(config_list["args"]) --uniform -e @(str(config["halt"]));
1712
1713		check_err_test(tgt_name, res);
1714	}
1715
1716	test sym(path.join(testdir, "no_required_arg_for_option")): bin
1717	{
1718		res := $ %(config_list["args"]) -f;
1719
1720		check_err_test(tgt_name, res);
1721	}
1722
1723	test sym(path.join(testdir, "no_required_arg_for_long_option")): bin
1724	{
1725		res := $ %(config_list["args"]) --file;
1726
1727		check_err_test(tgt_name, res);
1728	}
1729
1730	test sym(path.join(testdir, "given_arg_for_long_option_with_no_arg")): bin
1731	{
1732		res := $ %(config_list["args"]) --version=5;
1733
1734		check_err_test(tgt_name, res);
1735	}
1736
1737	test sym(path.join(testdir, "colon_option")): bin
1738	{
1739		res := $ %(config_list["args"]) -:;
1740
1741		check_err_test(tgt_name, res);
1742	}
1743
1744	test sym(path.join(testdir, "colon_long_option")): bin
1745	{
1746		res := $ %(config_list["args"]) --:;
1747
1748		check_err_test(tgt_name, res);
1749	}
1750
1751	test sym(path.join(testdir, "builtin_variable_arg_test")): bin
1752	{
1753		testdir: str = path.dirname(tgt_name);
1754		calc: str = path.basename(testdir);
1755
1756		extra: bool = bool(config["extra_math"]);
1757
1758		output: str =
1759		if extra
1760		{
1761			"14\n15\n16\n17.25\n";
1762		}
1763		else
1764		{
1765			"14\n15\n16\n";
1766		};
1767
1768		output_file: str = tgt_name +~ ".txt";
1769
1770		io.open(output_file, "w"): f
1771		{
1772			f.print(output);
1773		}
1774
1775		data: str =
1776		if extra
1777		{
1778			if calc == "bc"
1779			{
1780				"s=scale;i=ibase;o=obase;t=seed@2;ibase=A;obase=A;s;i;o;t;";
1781			}
1782			else
1783			{
1784				"J2@OIKAiAopRpRpRpR";
1785			}
1786		}
1787		else
1788		{
1789			if calc == "bc"
1790			{
1791				"s=scale;i=ibase;o=obase;ibase=A;obase=A;s;i;o;";
1792			}
1793			else
1794			{
1795				"OIKAiAopRpRpR";
1796			}
1797		};
1798
1799		args: []str =
1800		if extra
1801		{
1802			@[ "-S14", "-I15", "-O16", "-E17.25" ];
1803		}
1804		else
1805		{
1806			@[ "-S14", "-I15", "-O16" ];
1807		};
1808
1809		res1 := $ %(config_list["args"]) %(args) << $data;
1810		check_test(tgt_name, res1, output_file);
1811
1812		long_args: []str =
1813		if extra
1814		{
1815			@[ "--scale=14", "--ibase=15", "--obase=16", "--seed=17.25" ];
1816		}
1817		else
1818		{
1819			@[ "--scale=14", "--ibase=15", "--obase=16" ];
1820		};
1821
1822		res2 := $ %(config_list["args"]) %(long_args) << $data;
1823		check_test(tgt_name, res2, output_file);
1824	}
1825
1826	if calc == "bc"
1827	{
1828		io.open(path.join(testdir, OTHER_MATHLIB_SCALE_RESULTS_NAME), "w"): f
1829		{
1830			f.print("100\n");
1831		}
1832
1833		test sym(path.join(testdir, "builtin_var_arg_with_lib")): bin
1834		{
1835			testdir: str = path.dirname(tgt_name);
1836			results_file: str = path.join(testdir,
1837			                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1838
1839			res := $ %(config_list["args"]) -S100 -l << @("scale\n");
1840
1841			check_test(tgt_name, res, results_file);
1842		}
1843
1844		test sym(path.join(testdir, "builtin_variable_long_arg_with_lib")): bin
1845		{
1846			testdir: str = path.dirname(tgt_name);
1847			results_file: str = path.join(testdir,
1848			                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1849
1850			res := $ %(config_list["args"]) --scale=100 --mathlib <<
1851			         @("scale\n");
1852
1853			check_test(tgt_name, res, results_file);
1854		}
1855
1856		test sym(path.join(testdir, "builtin_var_arg_with_lib_env_arg")): bin
1857		{
1858			env.set env.str("BC_ENV_ARGS", "-l")
1859			{
1860				testdir: str = path.dirname(tgt_name);
1861				results_file: str = path.join(testdir,
1862				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1863
1864				res := $ %(config_list["args"]) -S100 << @("scale\n");
1865
1866				check_test(tgt_name, res, results_file);
1867			}
1868		}
1869
1870		test sym(path.join(testdir,
1871			               "builtin_var_long_arg_with_lib_env_arg")): bin
1872		{
1873			env.set env.str("BC_ENV_ARGS", "-l")
1874			{
1875				testdir: str = path.dirname(tgt_name);
1876				results_file: str = path.join(testdir,
1877				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1878
1879				res := $ %(config_list["args"]) --scale=100 << @("scale\n");
1880
1881				check_test(tgt_name, res, results_file);
1882			}
1883		}
1884
1885		test sym(path.join(testdir, "builtin_var_env_arg_with_lib_arg")): bin
1886		{
1887			env.set env.str("BC_ENV_ARGS", "-S100")
1888			{
1889				testdir: str = path.dirname(tgt_name);
1890				results_file: str = path.join(testdir,
1891				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1892
1893				res := $ %(config_list["args"]) -l << @("scale\n");
1894
1895				check_test(tgt_name, res, results_file);
1896			}
1897		}
1898
1899		test sym(path.join(testdir,
1900		                   "builtin_var_long_env_arg_with_lib_arg")): bin
1901		{
1902			env.set env.str("BC_ENV_ARGS", "--scale=100")
1903			{
1904				testdir: str = path.dirname(tgt_name);
1905				results_file: str = path.join(testdir,
1906				                              OTHER_MATHLIB_SCALE_RESULTS_NAME);
1907
1908				res := $ %(config_list["args"]) -l << @("scale\n");
1909
1910				check_test(tgt_name, res, results_file);
1911			}
1912		}
1913
1914		test sym(path.join(testdir, "limits")): bin
1915		{
1916			res := $ %(config_list["args"]) << @("limits\n");
1917
1918			check_test_retcode(tgt_name, res.exitcode);
1919
1920			if str(res.stdout) == "" || str(res.stdout) == "\n"
1921			{
1922				error("Test \"" +~ tgt_name +~ "\" did not produce output");
1923			}
1924		}
1925	}
1926
1927	test sym(path.join(testdir, "bad_arg_for_builtin_var_option")): bin
1928	{
1929		testdir: str = path.dirname(tgt_name);
1930		calc: str = path.basename(testdir);
1931
1932		scale: str = if calc == "bc" { "scale\n"; } else { "K\n"; };
1933
1934		res1 := $ %(config_list["args"]) --scale=18923c.rlg << $scale;
1935
1936		check_err_test(tgt_name, res1);
1937
1938		if bool(config["extra_math"])
1939		{
1940			seed: str = if calc == "bc" { "seed\n"; } else { "J\n"; };
1941
1942			res2 := $ %(config_list["args"]) --seed=18923c.rlg << $seed;
1943
1944			check_err_test(tgt_name, res2);
1945		}
1946	}
1947
1948	test sym(path.join(testdir, "directory_as_file")): bin
1949	{
1950		testdir: str = path.dirname(tgt_name);
1951
1952		res := $ %(config_list["args"]) $testdir;
1953
1954		check_err_test(tgt_name, res);
1955	}
1956
1957	test sym(path.join(testdir, "binary_file")): bin
1958	{
1959		res := $ %(config_list["args"]) @(file_dep);
1960
1961		check_err_test(tgt_name, res);
1962	}
1963
1964	test sym(path.join(testdir, "binary_stdin")): bin
1965	{
1966		res := $ %(config_list["args"]) < @(file_dep);
1967
1968		check_err_test(tgt_name, res);
1969	}
1970}
1971
1972fn register_timeconst_tests(
1973	bin: str,
1974	testdir: str,
1975	src_testdir: str,
1976) -> void
1977{
1978	timeconst: str = path.join(testdir, "scripts/timeconst.bc");
1979
1980	if !path.isfile(path.join(src_dir, timeconst))
1981	{
1982		io.eprint("Warning: " +~ timeconst +~ " does not exist\n");
1983		io.eprint(timeconst +~ " is not part of this bc because of " +~
1984		          "license incompatibility\n");
1985		io.eprint("To test it, get it from the Linux kernel at " +~
1986		          "`kernel/time/timeconst.bc`\n");
1987		io.eprint("Skipping...\n");
1988
1989		return;
1990	}
1991
1992	for i: range(1001)
1993	{
1994		test sym(path.join(timeconst, str(i)))
1995		{
1996			idx: str = path.basename(tgt_name) +~ "\n";
1997			file: str = path.join(src_dir, path.dirname(tgt_name));
1998
1999			// Generate.
2000			res1 := $ bc -q $file << $idx;
2001
2002			if res1.exitcode != 0
2003			{
2004				io.eprint("Other bc is not GNU compatible. Skipping...\n");
2005				return;
2006			}
2007
2008			// Run.
2009			res2 := $ %(config_list["args"]) -q $file << $idx;
2010
2011			if res2.exitcode != 0 || res2.stdout != res1.stdout
2012			{
2013				error("\nFailed on input: " +~ idx +~ "\n");
2014			}
2015		}
2016	}
2017}
2018
2019fn register_history_tests(
2020	bin: str,
2021	testdir: str,
2022	src_testdir: str,
2023) -> void
2024{
2025	calc: str = path.basename(testdir);
2026
2027	src_test_scriptdir: str = path.dirname(src_testdir);
2028
2029	len_res := $ @(path.join(src_test_scriptdir, "history.py")) $calc -a;
2030
2031	if len_res.exitcode != 0
2032	{
2033		io.eprint("Python 3 with pexpect doesn't work. Skipping history tests");
2034		return;
2035	}
2036
2037	len: usize = usize(str(len_res.stdout));
2038
2039	for i: range(len)
2040	{
2041		test sym(calc +~ "/history/" +~ str(i)): bin
2042		{
2043			name: str = tgt_name;
2044			parts: []str = name.split("/");
2045
2046			calc: str = parts[0];
2047			idx: str= parts[2];
2048
2049			src_testdir: str = path.join(src_dir, "tests");
2050
2051			$ @(path.join(src_testdir, "history.py")) -t $calc $idx @(file_dep);
2052		}
2053	}
2054}
2055
2056/**
2057 * Generates all of the test targets for an executable.
2058 * @param name  The base name of the executable.
2059 * @param targets  The targets that tests should depend on.
2060 */
2061fn exe_tests(name: str) -> void
2062{
2063	bin: str = exe_name(name);
2064
2065	testdir: str = path.join("tests", name);
2066	src_testdir: str = path.join(src_dir, testdir);
2067
2068	halt: str = if name == "bc" { "halt"; } else { "q"; };
2069	gen_options: []str = if name == "bc" { @[ "-lq" ]; };
2070	options: []str = if name == "bc" { @[ "-lqc" ]; } else { @[ "-xc" ]; };
2071
2072	other_num: str = "10000000000000000000000000000000000000000000000000" +~
2073	                 "0000000000000000000000000000";
2074	other_num70: str = "10000000000000000000000000000000000000000000000" +~
2075	                   "000000000000000000000\\\n0000000000";
2076
2077	other_tests: []str =
2078	if name == "bc"
2079	{
2080		@[ "x", "extended-register", "line_length()", other_num ];
2081	}
2082	else
2083	{
2084		@[ "l", "mathlib", "glpR", other_num +~ "pR" ];
2085	};
2086
2087	other_tests_results: []str = @[ other_num, other_num70 ];
2088
2089	var: str = name.toupper() +~ "_LINE_LENGTH";
2090
2091	script_options: []str =
2092	if name == "bc"
2093	{
2094		@[ "-lqC" ];
2095	}
2096	else
2097	{
2098		@[ "-xC" ];
2099	};
2100
2101	error_options: []str = if name == "bc" { @[ "-ls" ]; } else { @[ "-x" ]; };
2102
2103	args: []str =
2104	if bool(config["valgrind"])
2105	{
2106		VALGRIND_ARGS +~ @[ "./" +~ bin ];
2107	}
2108	else
2109	{
2110		@[ "./" +~ bin ];
2111	};
2112
2113	test_config: Gaml = @(gaml){
2114		args: $args
2115		halt: $halt
2116		gen_options: $gen_options
2117		options: $options
2118		script_options: $script_options
2119		error_options: $error_options
2120		other_tests: $other_tests
2121		other_tests_results: $other_tests_results
2122		var: $var
2123	};
2124
2125	push test_config: config_stack
2126	{
2127		extra: bool = bool(config["extra_math"]);
2128
2129		register_standard_tests(bin, testdir, src_testdir, extra);
2130		register_script_tests(bin, testdir, src_testdir, extra);
2131		register_stdin_tests(bin, testdir, src_testdir);
2132		register_read_tests(bin, testdir, src_testdir);
2133		register_error_tests(bin, testdir, src_testdir);
2134		register_other_tests(bin, testdir, src_testdir, extra);
2135
2136		if name == "bc" && bool(config["generated_tests"]) &&
2137		   path.isfile(path.join(src_testdir, "scripts/timeconst.bc"))
2138		{
2139			register_timeconst_tests(bin, testdir, src_testdir);
2140		}
2141
2142		if host.os != "Windows" && sym(config["history"]) == @builtin
2143		{
2144			register_history_tests(bin, testdir, src_testdir);
2145		}
2146	}
2147}
2148
2149/**
2150 * Gets the `$BINDIR`, including the `$DESTDIR`. This generates the default
2151 * value if it wasn't set.
2152 * @return  The `$BINDIR`, with the `$DESTDIR`.
2153 */
2154fn get_bindir() -> str
2155{
2156	temp: str = str(config["bindir"]);
2157
2158	bindir: str =
2159	if temp == ""
2160	{
2161		path.join(str(config["prefix"]), "bin");
2162	}
2163	else
2164	{
2165		temp;
2166	};
2167
2168	return path.join(DESTDIR, bindir);
2169}
2170
2171/**
2172 * Gets the `$LIBDIR`, including the `$DESTDIR`. This generates the default
2173 * value if it wasn't set.
2174 * @return  The `$LIBDIR`, with the `$DESTDIR`.
2175 */
2176fn get_libdir() -> str
2177{
2178	temp: str = str(config["libdir"]);
2179
2180	libdir: str =
2181	if temp == ""
2182	{
2183		path.join(str(config["prefix"]), "lib");
2184	}
2185	else
2186	{
2187		temp;
2188	};
2189
2190	return path.join(DESTDIR, libdir);
2191}
2192
2193/**
2194 * Gets the `$INCLUDEDIR`, including the `$DESTDIR`. This generates the default
2195 * value if it wasn't set.
2196 * @return  The `$INCLUDEDIR`, with the `$DESTDIR`.
2197 */
2198fn get_includedir() -> str
2199{
2200	temp: str = str(config["includedir"]);
2201
2202	includedir: str =
2203	if temp == ""
2204	{
2205		path.join(str(config["prefix"]), "include");
2206	}
2207	else
2208	{
2209		temp;
2210	};
2211
2212	return path.join(DESTDIR, includedir);
2213}
2214
2215/**
2216 * Gets the `$PC_PATH`, including the `$DESTDIR`. This generates the default
2217 * value if it wasn't set.
2218 * @return  The `$PC_PATH`, with the `$DESTDIR`.
2219 */
2220fn get_pc_path() -> str
2221{
2222	pc_path: str =
2223	if str(config["pc_path"]) == ""
2224	{
2225		res := $ pkg-config --variable=pc_path pkg-config;
2226
2227		str(res.stdout);
2228	}
2229	else
2230	{
2231		str(config["pc_path"]);
2232	};
2233
2234	return path.join(DESTDIR, pc_path);
2235}
2236
2237/**
2238 * Gets the `$DATAROOTDIR`, including the `$DESTDIR`. This generates the default
2239 * value if it wasn't set.
2240 * @return  The `$DATAROOTDIR`, with the `$DESTDIR`.
2241 */
2242fn get_datarootdir() -> str
2243{
2244	temp: str = str(config["datarootdir"]);
2245
2246	datarootdir: str =
2247	if temp == ""
2248	{
2249		path.join(str(config["prefix"]), "share");
2250	}
2251	else
2252	{
2253		temp;
2254	};
2255
2256	return path.join(DESTDIR, datarootdir);
2257}
2258
2259/**
2260 * Gets the `$DATADIR`, including the `$DESTDIR`. This generates the default
2261 * value if it wasn't set.
2262 * @return  The `$DATADIR`, with the `$DESTDIR`.
2263 */
2264fn get_datadir() -> str
2265{
2266	temp: str = str(config["datadir"]);
2267
2268	datadir: str =
2269	if temp == ""
2270	{
2271		get_datarootdir();
2272	}
2273	else
2274	{
2275		temp;
2276	};
2277
2278	return path.join(DESTDIR, datadir);
2279}
2280
2281/**
2282 * Gets the `$MANDIR`, including the `$DESTDIR`. This generates the default
2283 * value if it wasn't set.
2284 * @return  The `$MANDIR`, with the `$DESTDIR`.
2285 */
2286fn get_mandir() -> str
2287{
2288	temp: str = str(config["mandir"]);
2289
2290	mandir: str =
2291	if temp == ""
2292	{
2293		path.join(get_datadir(), "man");
2294	}
2295	else
2296	{
2297		temp;
2298	};
2299
2300	return path.join(DESTDIR, mandir);
2301}
2302
2303/**
2304 * Gets the `$MAN1DIR`, including the `$DESTDIR`. This generates the default
2305 * value if it wasn't set.
2306 * @return  The `$MAN1DIR`, with the `$DESTDIR`.
2307 */
2308fn get_man1dir() -> str
2309{
2310	temp: str = str(config["man1dir"]);
2311
2312	man1dir: str =
2313	if temp == ""
2314	{
2315		path.join(get_mandir(), "man1");
2316	}
2317	else
2318	{
2319		temp;
2320	};
2321
2322	return path.join(DESTDIR, man1dir);
2323}
2324
2325/**
2326 * Gets the `$MAN3DIR`, including the `$DESTDIR`. This generates the default
2327 * value if it wasn't set.
2328 * @return  The `$MAN3DIR`, with the `$DESTDIR`.
2329 */
2330fn get_man3dir() -> str
2331{
2332	temp: str = str(config["man3dir"]);
2333
2334	man3dir: str =
2335	if temp == ""
2336	{
2337		path.join(get_mandir(), "man3");
2338	}
2339	else
2340	{
2341		temp;
2342	};
2343
2344	return path.join(DESTDIR, man3dir);
2345}
2346