diff --git a/lib/std/io/file.c3 b/lib/std/io/file.c3 index 4848cd363..95da59d23 100644 --- a/lib/std/io/file.c3 +++ b/lib/std/io/file.c3 @@ -19,6 +19,11 @@ fn File! open_path(Path path, String mode) return from_handle(os::native_fopen(path.str_view(), mode)); } +fn bool exists(String file) => @pool() +{ + return path::exists(path::temp_new(file)) ?? false; +} + fn File from_handle(CFile file) { return { .file = file }; @@ -186,11 +191,15 @@ fn char[]! load_new(String filename, Allocator allocator = allocator::heap()) return data[:len]; } +fn char[]! load_path_new(Path path, Allocator allocator = allocator::heap()) => load_new(path.str_view(), allocator); + fn char[]! load_temp(String filename) { return load_new(filename, allocator::temp()); } +fn char[]! load_path_temp(Path path) => load_temp(path.str_view()); + fn void! save(String filename, char[] data) { File file = open(filename, "wb")!; diff --git a/lib/std/os/subprocess.c3 b/lib/std/os/subprocess.c3 index b1f90e16f..9227bdac2 100644 --- a/lib/std/os/subprocess.c3 +++ b/lib/std/os/subprocess.c3 @@ -362,6 +362,11 @@ fn File SubProcess.stdout(&self) return file::from_handle(self.stdout_file); } +fn File SubProcess.stderr(&self) +{ + return file::from_handle(self.stderr_file); +} + fn CInt! SubProcess.join(&self) @if(env::WIN32) { if (self.stdin_file) diff --git a/releasenotes.md b/releasenotes.md index 35fe7399d..8f1964b78 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -8,6 +8,7 @@ - `{| |}` expression blocks deprecated. ### Fixes +- Bug appearing when `??` was combined with boolean in some cases. ### Stdlib changes diff --git a/src/compiler/llvm_codegen_expr.c b/src/compiler/llvm_codegen_expr.c index c20982ff4..91ad9767b 100644 --- a/src/compiler/llvm_codegen_expr.c +++ b/src/compiler/llvm_codegen_expr.c @@ -1247,7 +1247,21 @@ void llvm_set_phi(LLVMValueRef phi, LLVMValueRef val1, LLVMBasicBlockRef block1, void llvm_new_phi(GenContext *c, BEValue *value, const char *name, Type *type, LLVMValueRef val1, LLVMBasicBlockRef block1, LLVMValueRef val2, LLVMBasicBlockRef block2) { - LLVMValueRef phi = LLVMBuildPhi(c->builder, LLVMTypeOf(val1), name); + LLVMTypeRef ret_type = LLVMTypeOf(val1); + LLVMTypeRef other_type = LLVMTypeOf(val2); + if (ret_type != other_type) + { + if (ret_type == c->bool_type) + { + val2 = LLVMBuildTrunc(c->builder, val2, ret_type, ""); + } + else + { + assert(other_type == c->bool_type); + val2 = LLVMBuildZExt(c->builder, val2, ret_type, ""); + } + } + LLVMValueRef phi = LLVMBuildPhi(c->builder, ret_type, name); llvm_set_phi(phi, val1, block1, val2, block2); llvm_value_set(value, phi, type); } diff --git a/test/src/tester.c3 b/test/src/tester.c3 new file mode 100644 index 000000000..15b92c234 --- /dev/null +++ b/test/src/tester.c3 @@ -0,0 +1,406 @@ +import std::io; +import std::io::file; +import libc; +import std::math; +import std::collections::map; +import std::collections::list; +import std::core::dstring; +import std::os::process; + +def StringMap = HashMap{String, String}; +def StringList = List{String}; + +String appname; +Path compiler_path; +int test_count; +int skip_count; +int success_count; +Path start_cwd; +Path test_dir; + +fn void arg_error_exit(String fmt, args...) @noreturn +{ + io::printfn(fmt, ...args); + usage(); +} + +fn void error_exit(String fmt, args...) @noreturn +{ + io::printfn(fmt, ...args); + libc::exit(1); +} + + +fn void main(String[] args) +{ + appname = args[0]; + args = { "foo", "../cmake-build-debug-latest/c3c", "test_suite/" }; + if (args.len < 3 || args.len > 4) usage(); + + // Check compiler path + start_cwd = path::temp_cwd()!!; + test_dir = start_cwd.temp_append("_c3test")!!; + Path! path = start_cwd.temp_append(args[1]); + if (catch path) arg_error_exit("Invalid compiler path: %s", args[1]); + + if (!path::is_file(path)) + { + error_exit("Error: Invalid path to compiler: %s", path.path_string); + } + compiler_path = path; + + bool only_skipped = args.len == 4; + + if (only_skipped && args[3] != "-s" && args[3] != "--skipped") usage(); + + Path! file = path::temp_new(args[2]); + if (catch file) arg_error_exit("Invalid path: '%s'.", args[2]); + + switch + { + case path::is_file(file): + test_file(file); + case path::is_dir(file): + test_path(file)!!; + default: + error_exit("Error: Path wasn't to directory or file: %s", file); + } + + io::printfn("Found %d tests: %.1f%% (%d / %d) passed (%d skipped).", + test_count, 100 * success_count / math::max(1, test_count - skip_count), + success_count, test_count - skip_count, skip_count); + libc::exit(success_count == test_count - skip_count ? 0 : 1); +} + +struct Error +{ + int line; + String text; +} +struct RunFile +{ + String name; + File file; + int line_offset; + List{String} expected_lines; + List{Error} warnings; + List{Error} errors; + bool is_llvm; +} + +fn void RunFile.add_line(&self, String line) +{ + if (self.is_llvm) + { + line = line.trim(); + if (line == "") return; + self.expected_lines.push(line); + } + else + { + io::fprintn(&self.file, line)!!; + } +} + +fn RunFile*! create_file(String filename, bool llvm = false) +{ + File file = file::open_path(test_dir.temp_append(filename), "wb")!; + RunFile *run_file = mem::temp_new(RunFile, { .name = filename, .file = file, .is_llvm = llvm }); + run_file.warnings.temp_init(); + run_file.errors.temp_init(); + if (llvm) run_file.expected_lines.temp_init(); + return run_file; +} + +fn void RunFile.close(&self) +{ + (void)self.file.close(); +} + +struct RunSettings +{ + bool safe; + bool debuginfo; + bool no_deprecation; + List{String} opts; + String arch; + RunFile* current_file; + List{RunFile*} files; +} + +fn bool check_line(RunSettings*, String type, String file, String line, String col, String message) +{ + return false; +/* + def check_line(self, typ, file, line, col, message): + map_ = {} + if typ == 'Error': + map_ = self.errors + elif typ == 'Warning': + map_ = self.warnings + else: + self.exit_error("Unknown type: " + typ) + file = os.path.basename(file) + key = file + ":" + line + value = map_.get(key) + if value is None: + return False + if value in message: + del map_[key] + return True + else: + return False +*/ +} +fn bool parse_result(DString out, RunSettings settings) +{ + // Inefficient, fix. + bool success = true; + foreach (line : out.str_view().tsplit("\n")) + { + if (!line) continue; + String[] parts = line.tsplit("|", 5); + if (parts.len != 5) + { + error_exit("Unexpected error: %s", out); + } + if (!check_line(&settings, ...parts[:5])) + { + parts[0].convert_ascii_to_lower(); + io::printf("Unexpected %s in %s line %s:", parts[0], path::temp_new(parts[1]).basename()!!, parts[2]); + io::printfn(`"%s"`, parts[4]); + success = false; + } + } + int not_found_errors, not_found_warnings; + foreach (file : settings.files) + { + if (file.errors.len()) + { + success = false; + if (!not_found_errors) io::printn("Errors that never occurred:"); + foreach (i, &item : file.errors) + { + io::printfn(`%d. %s %d expected: "%s"`, ++not_found_errors, file.name, item.line, item.text); + } + } + } + foreach (file : settings.files) + { + if (file.warnings.len()) + { + success = false; + if (!not_found_warnings) io::printn("Warnings that never occurred:"); + foreach (i, &item : file.warnings) + { + io::printfn(`%d. %s %d expected: "%s"`, ++not_found_warnings, file.name, item.line, item.text); + } + } + } + return success; +} + +fn void parse_trailing_directive(int line_number, String line, RunSettings* settings, bool is_single) +{ + usz index = line.rindex_of("// #")!! + 4; + line = line[index..].trim(); + switch + { + case line.starts_with("warning:"): + line = line[8..].trim(); + settings.current_file.warnings.push({ line_number, line }); + case line.starts_with("error:"): + line = line[6..].trim(); + settings.current_file.errors.push({ line_number, line }); + default: + error_exit("Unknown trailing directive '%s'", line); + } +} +fn void parse_header_directive(int line_no, String line, RunSettings* settings, bool is_single) +{ + line = line[4..].trim(); + switch + { + case line.starts_with("error:"): + line = line[6..].trim(); + settings.current_file.errors.push({ line_no, line }); + case line.starts_with("safe:"): + settings.safe = line[5..].trim() == "yes"; + case line.starts_with("debuginfo:"): + settings.debuginfo = line[10..].trim() == "yes"; + case line.starts_with("opt:"): + settings.opts.push(line[4..].trim()); + case line.starts_with("target:"): + settings.arch = line[7..].trim(); + case line.starts_with("deprecation:"): + settings.no_deprecation = line[12..].trim() == "no"; + case line.starts_with("file:"): + if (is_single) error_exit("'file' directive only allowed with .c3t"); + settings.current_file.close(); + line = line[5..].trim(); + RunFile* file = settings.current_file = create_file(line)!!; + settings.files.push(file); + settings.current_file = file; + case line.starts_with("expect:"): + if (is_single) error_exit("'expect' directive only allowed with .c3t"); + line = line[7..].trim(); + settings.current_file.close(); + RunFile* file = settings.current_file = create_file(line, llvm: true)!!; + settings.files.push(file); + default: + io::printfn("Unknown header directive '%s'", line); + libc::exit(1); + } +} + +fn void test_file(Path file_path) +{ + io::printfn("Checking %s", file_path); + static int count = 1; + bool single; + (void)path::rmtree(test_dir); + if (@catch(path::mkdir(test_dir))) + { + io::printfn("Failed to create temp test directory '%s'.", test_dir); + libc::exit(1); + } + switch (file_path.extension() ?? "") + { + case "c3": + single = true; + case "c3t": + single = false; + default: + io::printfn("Unexpected file name '%s', expected a file with a '.c3' or '.c3t' suffix.", file_path.str_view()); + libc::exit(1); + } + File! f = file::open_path(file_path, "rb"); + if (catch f) + { + io::printfn("Error: Failed to open '%s'.", file_path); + libc::exit(1); + } + defer (void)f.close(); + RunSettings settings; + settings.opts.temp_init(); + settings.files.temp_init(); + settings.current_file = create_file(file_path.basename()[..^(single ? 4 : 5)].tconcat(".c3"))!!; + settings.files.push(settings.current_file); + int line_no = 0; + while (try line = io::treadline(&f)) + { + if (line.starts_with("// #") || line.starts_with("/* #")) + { + parse_header_directive(line_no, line, &settings, single); + continue; + } + else if (line.contains("// #")) + { + parse_trailing_directive(line_no, line, &settings, single); + continue; + } + settings.current_file.add_line(line); + line_no++; + } + settings.current_file.close(); + test_count += 1; + DString cmdline = dstring::temp_new(compiler_path.str_view()); + cmdline.append(" compile-only --test "); + foreach (file : settings.files) + { + if (file.is_llvm) continue; + cmdline.append(file.name); + cmdline.append(" "); + } + if (!single) cmdline.append("--emit-llvm "); + cmdline.append(settings.debuginfo ? "-g " : "-g0 "); + if (settings.arch) cmdline.appendf("--target %s ", settings.arch); + cmdline.append("-O0 "); + if (settings.no_deprecation) cmdline.append("--silence-deprecation "); + cmdline.append(settings.safe ? "--safe=yes " : "--safe=no "); + foreach (opt : settings.opts) + { + cmdline.appendf("%s ", opt); + } + io::printfn("Dir: %s", test_dir); + path::chdir(test_dir)!!; + io::printn(cmdline); + SubProcess compilation = process::create(cmdline.str_view().trim().tsplit(" "), { .search_user_path = true })!!; + CInt result = compilation.join()!!; + DString out = dstring::temp_new(); + io::copy_to(&&compilation.stderr(), &out)!!; + if (result != 0 && result != 1) + { + io::printfn("Error(%s): ", result, out); + return; + } + if (!parse_result(out, settings)) return; + foreach (file : settings.files) + { + if (!file.is_llvm) continue; + if (!file::exists(file.name)) + { + io::printfn("Did not compile file %s.", file.name); + return; + } + File file_ll = file::open(file.name, "rb")!!; + defer (void)file_ll.close(); + String! next = file.expected_lines.pop_first(); + while (try line = io::treadline(&file_ll) && try value = next) + { + line = line.trim(); + if (line == "") continue; + if (line.contains(value)) + { + next = file.expected_lines.pop_first(); + } + } + if (try next) + { + io::printfn(`%s did not contain: "%s"`, file.name, next); + io::printfn("\n\n\n---------------------------------------------------> %s\n\n", file.name); + (void)file_ll.seek(0); + (void)io::printn((String)io::read_new_fully(&file_ll, allocator: allocator::temp())); + io::printfn("<---------------------------------------------------- %s\n", file_path); + return; + } + } + success_count++; +} + +fn void! test_path(Path file_path) +{ + (void)path::chdir(start_cwd); + foreach (file : path::temp_ls(file_path)!!) + { + @pool() + { + (void)path::chdir(start_cwd); + file = file_path.temp_append(file.str_view())!; + switch + { + case path::is_dir(file): + test_path(file)!; + case path::is_file(file): + switch (file.extension() ?? "") + { + case "c3": + case "c3t": + test_file(file); + } + default: + io::printfn("Skip %s", file); + // Ignore + } + }; + } +} + +fn void usage() @noreturn +{ + io::printfn("Usage: %s [-s]", appname); + io::printn(); + io::printn("Options:"); + io::printn(" -s, --skipped only run skipped tests"); + libc::exit(0); +} diff --git a/test/test_suite/enumerations/inc_assign_fail.c3 b/test/test_suite/enumerations/inc_assign_fail.c3 index 25ce04994..f31877c7e 100644 --- a/test/test_suite/enumerations/inc_assign_fail.c3 +++ b/test/test_suite/enumerations/inc_assign_fail.c3 @@ -18,7 +18,3 @@ fn void main() Bob gh; gh += 1; // #error: A value of type 'Bob' } - -/* #expect: test.ll - -feofke \ No newline at end of file diff --git a/test/test_suite/expressions/casts/void_casting.c3 b/test/test_suite/expressions/casts/void_casting.c3 deleted file mode 100644 index 808b6c1c7..000000000 --- a/test/test_suite/expressions/casts/void_casting.c3 +++ /dev/null @@ -1,88 +0,0 @@ -// #target: macos-x64 -module test; - -fn void! foo() -{} - -fault Abc { AAA } - -macro int! bar() -{ - return Abc.AAA?; -} - -fn int! baz() -{ - return 2; -} - -fn int main() -{ - int x; - int! y; - (void)x; - (void)y; - (void)foo(); - (void)bar(); - (void)(bar() + 1); - (void)baz(); - (void)(baz() + 1); - return 1; -} - -/* #expect: test.ll -define i32 @main() #0 { -entry: - %x = alloca i32, align 4 - %y = alloca i32, align 4 - %y.f = alloca i64, align 8 - %retparam = alloca i32, align 4 - %retparam9 = alloca i32, align 4 - store i32 0, ptr %x, align 4 - store i64 0, ptr %y.f, align 8 - store i32 0, ptr %y, align 4 - %0 = load i32, ptr %x, align 4 - %optval = load i64, ptr %y.f, align 8 - %not_err = icmp eq i64 %optval, 0 - br i1 %not_err, label %after_check, label %end_block - -after_check: ; preds = %entry - %1 = load i32, ptr %y, align 4 - br label %end_block - -end_block: ; preds = %after_check, %entry - %2 = call i64 @test.foo() - %not_err1 = icmp eq i64 %2, 0 - br i1 %not_err1, label %after_check2, label %end_block3 - -after_check2: ; preds = %end_block - br label %end_block3 - -end_block3: ; preds = %after_check2, %end_block - br label %end_block4 - -end_block4: ; preds = %end_block3 - br label %end_block5 - -end_block5: ; preds = %end_block4 - %3 = call i64 @test.baz(ptr %retparam) - %not_err6 = icmp eq i64 %3, 0 - br i1 %not_err6, label %after_check7, label %end_block8 - -after_check7: ; preds = %end_block5 - %4 = load i32, ptr %retparam, align 4 - br label %end_block8 - -end_block8: ; preds = %after_check7, %end_block5 - %5 = call i64 @test.baz(ptr %retparam9) - %not_err10 = icmp eq i64 %5, 0 - br i1 %not_err10, label %after_check11, label %end_block12 - -after_check11: ; preds = %end_block8 - %6 = load i32, ptr %retparam9, align 4 - %add = add i32 %6, 1 - br label %end_block12 - -end_block12: ; preds = %after_check11, %end_block8 - ret i32 1 -} diff --git a/test/test_suite/expressions/casts/void_casting.c3t b/test/test_suite/expressions/casts/void_casting.c3t new file mode 100644 index 000000000..8cdcff296 --- /dev/null +++ b/test/test_suite/expressions/casts/void_casting.c3t @@ -0,0 +1,62 @@ +// #target: macos-x64 +module test; + +fn void! foo() +{} + +fault Abc { AAA } + +macro int! bar() +{ + return Abc.AAA?; +} + +fn int! baz() +{ + return 2; +} + +fn int main() +{ + int x; + int! y; + (void)x; + (void)y; + (void)foo(); + (void)bar(); + (void)(bar() + 1); + (void)baz(); + (void)(baz() + 1); + return 1; +} + +/* #expect: test.ll + +define i32 @main() #0 { +entry: + %x = alloca i32, align 4 + %y = alloca i32, align 4 + %y.f = alloca i64, align 8 + %retparam = alloca i32, align 4 + %retparam2 = alloca i32, align 4 + store i32 0, ptr %x, align 4 + store i64 0, ptr %y.f, align 8 + store i32 0, ptr %y, align 4 + %optval = load i64, ptr %y.f, align 8 + %0 = call i64 @test.foo() + br label %postfailed +postfailed: ; preds = %entry + br label %postfailed1 +postfailed1: ; preds = %postfailed + %1 = call i64 @test.baz(ptr %retparam) + %2 = call i64 @test.baz(ptr %retparam2) + %not_err = icmp eq i64 %2, 0 + %3 = call i1 @llvm.expect.i1(i1 %not_err, i1 true) + br i1 %3, label %after_check, label %voiderr +after_check: ; preds = %postfailed1 + %4 = load i32, ptr %retparam2, align 4 + %add = add i32 %4, 1 + br label %voiderr +voiderr: ; preds = %after_check, %postfailed1 + ret i32 1 +} \ No newline at end of file