Rename of files, improved for handling, add cb hints

- Rename Test to Ink as main file
- Support more errors for for loops.
- Add hints to cbs
This commit is contained in:
2025-09-02 11:55:27 +02:00
parent 9e0728f952
commit 603b625e21
10 changed files with 114 additions and 35 deletions

693
Ink.jai Normal file
View File

@@ -0,0 +1,693 @@
/////////////////////////////////////
/*~ nbr: General improvements
- [x] Print out all failed tests in a list at the end
- [x] Use new compiler API with Compile_Result and Compiled_File instead
- [ ] Use unix (posix? bash? ascii?) color codes for errors
- [ ] Print golden file as green and new output as red
- [ ] Rename to Ink.jai
- [ ] Add -test option. -test does the same as test.exe used to do
- [ ] Add -fuzz option to run fuzzer (add args later)
- [ ] Add -output option to output the compiled file. Issue with this is the generated data can't be output like that. Would require serialization.
*/
#import "Basic";
#import "File";
#import "String";
#import "File_Utilities";
#import "Print_Color";
#load "module.jai";
GOLDEN_EXTENSION :: "golden";
LEXER_FOLDER :: "lex";
PARSER_FOLDER :: "parse";
CODEGEN_FOLDER :: "codegen";
COMPILED_FOLDER :: "compiled";
SEMANTIC_ANALYSIS_FOLDER :: "semant";
TESTS_FOLDER :: "test";
SHADER_EXTENSION :: "ink";
SUITE_EXTENSION :: "suite";
Stage_Flags :: enum_flags u16 {
Lexer :: 0x1;
Parser :: 0x2;
Semantic_Analysis :: 0x4;
Codegen :: 0x8;
Compile :: 0x10;
}
Output_Type :: enum_flags u16 {
Golden :: 0x1;
StdOut :: 0x2;
}
Result_Type :: enum {
File_Read_Failed;
Golden_File_Not_Found;
StdOut;
Golden_Output;
Passed;
Failed;
}
Result :: struct {
type : Result_Type;
path : string;
stage : Stage_Flags;
golden_path : string;
info_text : string;
}
Test_Case :: struct {
path : string;
stage_flags : Stage_Flags;
}
Test_Suite :: struct {
name : string;
test_cases : [..]Test_Case;
results : [..]Result;
}
get_golden_path :: (file_path : string, stage : Stage_Flags, allocator := context.allocator) -> string {
path := parse_path(file_path);
file_without_extension := split(path.words[path.words.count - 1], ".");
builder : String_Builder;
builder.allocator = temp;
final_path_length := file_path.count - SHADER_EXTENSION.count + GOLDEN_EXTENSION.count + 1; // +1 for dot
path.words.count -= 1;
if stage == {
case .Lexer; {
dir := tprint("%/%", TESTS_FOLDER, LEXER_FOLDER);
make_directory_if_it_does_not_exist(dir);
array_add(*path.words, LEXER_FOLDER);
}
case .Parser; {
dir := tprint("%/%", TESTS_FOLDER, PARSER_FOLDER);
make_directory_if_it_does_not_exist(dir);
array_add(*path.words, PARSER_FOLDER);
}
case .Semantic_Analysis; {
dir := tprint("%/%", TESTS_FOLDER, SEMANTIC_ANALYSIS_FOLDER);
make_directory_if_it_does_not_exist(dir);
array_add(*path.words, SEMANTIC_ANALYSIS_FOLDER);
}
case .Codegen; {
dir := tprint("%/%", TESTS_FOLDER, CODEGEN_FOLDER);
make_directory_if_it_does_not_exist(dir);
array_add(*path.words, CODEGEN_FOLDER);
}
case .Compile; {
dir := tprint("%/%", TESTS_FOLDER, COMPILED_FOLDER);
make_directory_if_it_does_not_exist(dir);
array_add(*path.words, COMPILED_FOLDER);
}
}
init_string_builder(*builder, file_without_extension.count + GOLDEN_EXTENSION.count + 1);
append(*builder, file_without_extension[0]);
append(*builder, ".");
append(*builder, GOLDEN_EXTENSION);
golden_path := builder_to_string(*builder);
array_add(*path.words, golden_path);
final_path := path_to_string(path);
return final_path;
}
do_golden_comparison :: (golden_path : string, comparison_text : string, result_data : *Result, output_type : Output_Type) {
if output_type & .Golden {
// Output the comparison file
write_entire_file(golden_path, comparison_text);
result_data.golden_path = copy_string(golden_path);
result_data.type = .Golden_Output;
return;
} else {
// Do the comparison
if !file_exists(golden_path) {
result_data.info_text = tprint("Golden file % does not exist. Please run with -output-as-golden at least once.\n", golden_path);
result_data.type = .Golden_File_Not_Found;
return;
}
golden_text, ok := read_entire_file(golden_path);
if !ok {
result_data.info_text = tprint("Unable to open golden file %\n", golden_path);
result_data.type = .Golden_File_Not_Found;
return;
}
comp := replace(comparison_text, "\r\n", "\n");
gold := replace(golden_text, "\r\n", "\n");
result := compare(comp, gold) == 0;
if !result {
result_data.type = .Failed;
result_data.info_text = tprint("Golden file:\n%\n===============\n%", gold, comp);
} else {
result_data.type = .Passed;
}
}
}
run_codegen_test :: (file_path : string, result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result.file = make_file(result, file_path);
result.allocator = make_arena(*result.arena);
result_data : Result;
result_data.path = file_path;
lex(result);
parse(result);
check(result);
if result.had_error {
result_data.type = .Failed;
return result_data;
}
result_data = run_codegen_test(result, output_type);
return result_data;
}
run_codegen_test :: (result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result_data : Result;
result_data.path = result.file.path;
result_text : string;
codegen(result);
if result.had_error {
result_data.type = .Failed;
result_text = report_messages(result.messages);
return result_data;
}
result_text = result.codegen_result_text;
if output_type & .StdOut {
result_data.info_text = result_text;
result_data.type = .StdOut;
return result_data;
}
golden_path := get_golden_path(result.file.path, .Codegen);
do_golden_comparison(golden_path, result_text, *result_data, output_type);
return result_data;
}
run_compile_test :: (path : string, output_type : Output_Type = 0) -> Result, Compile_Result {
compiler : Shader_Compiler;
result : Result;
compilation_result := compile_file(*compiler, path);
print("\n");
if compilation_result.had_error {
result.type = .Failed;
result.info_text = tprint("Failed compiling: %\n", path);
}
return result, compilation_result;
}
run_lexer_test :: (file_path : string, result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result_data : Result;
result_data.path = file_path;
result_data.stage = .Lexer;
result_text : string;
result.file = make_file(result, file_path);
result.allocator = make_arena(*result.arena);
lex(result);
if result.had_error {
result_data.type = .Failed;
result_text = report_messages(result.messages);
} else {
result_text = pretty_print_tokens(result.tokens.tokens, *temp);
}
if output_type & .StdOut {
result_data.info_text = result_text;
result_data.type = .StdOut;
return result_data;
}
golden_path := get_golden_path(file_path, .Lexer);
do_golden_comparison(golden_path, result_text, *result_data, output_type);
return result_data;
}
run_parser_test :: (file_path : string, result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result_data : Result;
result_data.path = file_path;
result.file = make_file(result, file_path);
result.allocator = make_arena(*result.arena);
lex(result);
if result.had_error {
result_data.type = .Passed;
return result_data;
}
result_data = run_parser_test(result, output_type);
return result_data;
}
run_parser_test :: (result : *Compile_Result, output_type : Output_Type = 0) -> Result {
parse(result);
result_data : Result;
result_data.path = result.file.path;
result_text : string;
if result.had_error {
result_data.type = .Failed;
result_text = report_messages(result.messages,, temp);
} else {
result_text = pretty_print_ast(result.root, *temp);
}
if output_type & .StdOut {
result_data.info_text = result_text;
result_data.type = .StdOut;
return result_data;
}
golden_path := get_golden_path(result.file.path, .Parser);
do_golden_comparison(golden_path, result_text, *result_data, output_type);
return result_data;
}
run_semantic_analysis_test :: (result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result_data : Result;
result_data.path = result.file.path;
result_text : string;
check(result);
if result.had_error {
result_data.type = .Failed;
result_text = report_messages(result.messages);
} else {
result_text = pretty_print_symbol_table(result, temp);
}
if output_type & .StdOut {
result_data.info_text = result_text;
result_data.type = .StdOut;
return result_data;
}
golden_path := get_golden_path(result.file.path, .Semantic_Analysis);
do_golden_comparison(golden_path, result_text, *result_data, output_type);
return result_data;
}
run_semantic_analysis_test :: (file_path : string, result : *Compile_Result, output_type : Output_Type = 0) -> Result {
result.file = make_file(result, file_path);
result.allocator = make_arena(*result.arena);
result_data : Result;
result_data.path = file_path;
lex(result);
parse(result);
if result.had_error {
result_data.type = .Failed;
return result_data;
}
result_data = run_semantic_analysis_test(result, output_type);
return result_data;
}
make_test_case :: (path : string, stage_flags : Stage_Flags, allocator := context.allocator) -> Test_Case {
test_case : Test_Case;
test_case.path = copy_string(path,, allocator);
replace_chars(test_case.path, "\\", #char "/");
test_case.stage_flags = stage_flags;
return test_case;
}
run_test_new :: (file_path : string, stage_flags : Stage_Flags, results : *[..]Result, output_type : Output_Type = 0) {
compile_result : Compile_Result;
compile_result.file = make_file(*compile_result, file_path);
compile_result.allocator = make_arena(*compile_result.arena);
result : Result;
if stage_flags & .Lexer {
result = run_lexer_test(file_path, *compile_result, output_type);
record_result(results, result);
}
if stage_flags & .Parser {
if stage_flags & .Lexer && result.type == .Passed || result.type == .Golden_Output {
result = run_parser_test(*compile_result, output_type);
} else {
result = run_parser_test(file_path, *compile_result, output_type);
}
record_result(results, result);
}
if stage_flags & .Semantic_Analysis {
if stage_flags & .Parser && (result.type == .Passed || result.type == .Golden_Output) {
result = run_semantic_analysis_test(*compile_result, output_type);
} else {
result = run_semantic_analysis_test(file_path, *compile_result, output_type);
}
record_result(results, result);
}
if stage_flags & .Codegen {
if stage_flags & .Semantic_Analysis && (result.type == .Passed || result.type == .Golden_Output) {
result = run_codegen_test(*compile_result, output_type);
} else {
result = run_codegen_test(file_path, *compile_result, output_type);
}
record_result(results, result);
}
if stage_flags & .Compile {
result = run_compile_test(file_path, output_type);
}
}
run_test :: (test_case : Test_Case, results : *[..]Result, output_type : Output_Type = 0) {
print("%Running test: %......", cyan(), test_case.path);
// path 30
// len 35
// == 5
// path 20
// len = 35
// == 15
len := 50;
rest := len - test_case.path.count;
for i: 0..rest {
print(" ");
}
run_test_new(test_case.path, test_case.stage_flags, results, output_type);
// run_test(test_case.path, test_case.stage_flags, results, output_type);
}
record_result :: (results : *[..]Result, result : Result) {
array_add(results, result);
}
run_test_suite :: (using suite : *Test_Suite, output_type : Output_Type = 0) {
if suite.name.count > 0 {
print("%Running suite: %\n", green(), suite.name);
print("%", reset_color());
}
Fail_Data :: struct {
path : string;
stage : string;
}
failed_test_paths : [..]Fail_Data;
failed_test_paths.allocator = temp;
builder : String_Builder;
init_string_builder(*builder,, temp);
for test_case : test_cases {
run_test(test_case, *suite.results, output_type);
for < suite.results {
result := suite.results[it_index];
if compare(result.path, test_case.path) == 0 {
if result.type == {
case .Failed; {
array_add(*failed_test_paths, .{ result.path, stage_to_string(result.stage) });
}
case .File_Read_Failed; {
array_add(*failed_test_paths, .{ result.path, "file not found" });
}
case .Golden_File_Not_Found; {
array_add(*failed_test_paths, .{ result.path, tprint("golden file not found for %", stage_to_string(result.stage)) });
}
}
evaluate_result(result);
} else {
break;
}
}
// print("\n");
}
append(*builder, "\n");
if output_type == 0 {
if failed_test_paths.count == 0 {
green(*builder);
print_to_builder(*builder, "All % tests passed!\n", test_cases.count);
reset_color(*builder);
} else {
print_to_builder(*builder, "%/% tests passed\n", test_cases.count - failed_test_paths.count, test_cases.count);
red(*builder);
print_to_builder(*builder, "% failed\n", failed_test_paths.count);
for failed_test : failed_test_paths {
print_to_builder(*builder, "% failed with error: %\n", failed_test.path, failed_test.stage);
}
reset_color(*builder);
}
}
print("%\n", builder_to_string(*builder));
}
read_suite :: (file_path : string, suite : *Test_Suite) -> bool {
bytes, ok := read_entire_file(file_path);
if !ok {
log_error("Unable to read suite file %\n", file_path);
return false;
}
path := parse_path(file_path);
file_without_extension := split(path.words[path.words.count - 1], ".");
suite.name = copy_string(file_without_extension[0]);
split_lines := split(bytes, "\n");
for split_line : split_lines {
line := split(split_line, " ");
if line[0].count == 0 {
continue;
}
if line[0].data[0] == #char "#" {
continue;
}
if line.count == 1 {
line = split(split_line, "\t");
if line.count == 1 {
log_error("Invalid line - % - \n", it_index + 1);
continue;
}
}
test_case_path := line[0];
stage_flags : Stage_Flags;
for i: 0..line.count - 1 {
trimmed := trim(line[i]);
if equal(trimmed, "lex") {
stage_flags |= .Lexer;
} else if equal(trimmed, "parse") {
stage_flags |= .Parser;
} else if equal(trimmed, "semant") {
stage_flags |= .Semantic_Analysis;
} else if equal(trimmed, "codegen") {
stage_flags |= .Codegen;
} else if equal(trimmed, "compile") {
stage_flags |= .Compile;
}
}
test_case := make_test_case(test_case_path, stage_flags);
array_add(*suite.test_cases, test_case);
}
return true;
}
read_test :: () {
}
stage_to_string :: (stage : Stage_Flags) -> string {
if #complete stage == {
case .Lexer; return "lexing";
case .Parser; return "parsing";
case .Semantic_Analysis; return "semantic checking";
case .Codegen; return "codegen";
case .Compile; return "compiled";
case; return "";
}
}
evaluate_result :: (result : Result) {
stage : string = stage_to_string(result.stage);
if #complete result.type == {
case .File_Read_Failed; {
print(" %", red());
print("failed with File_Read_Failed\n");
}
case .Golden_File_Not_Found; {
print(" %", red());
print("failed with Golden File Not Found for stage %\n", stage);
}
case .StdOut; {
}
case .Golden_Output; {
print(" %", yellow());
print("output new golden file at %\n", result.golden_path);
}
case .Passed; {
print(" %", green());
print("passed %\n", stage);
}
case .Failed; {
print(" %", red());
print("failed %\n", stage);
}
}
if result.info_text.count > 0 {
print("%", cyan());
print("--- Info text ---\n");
print("%", yellow());
print("%\n", result.info_text);
}
print("%", reset_color());
}
main :: () {
args := get_command_line_arguments();
suites : [..]Test_Suite;
output_type : Output_Type = 0;
Argument_Parse_State :: enum {
None;
Compile;
Run_Suite;
Run_Test;
}
arg_parse_state : Argument_Parse_State;
current_suite : *Test_Suite;
path : string;
for i: 1..args.count - 1 {
arg := args[i];
if arg == "-output-as-golden" {
output_type |= .Golden;
continue;
} else if arg == "-output" {
output_type |= .StdOut;
continue;
}
if arg_parse_state == {
case .Run_Suite; {
if arg == "-output-as-golden" {
output_type |= .Golden;
} else if arg == "-output" {
output_type |= .StdOut;
} else {
print("%Unknown argument % %\n", red(), arg, reset_color());
}
}
case .Run_Test; {
cases := current_suite.test_cases.count;
if arg == "-lex" {
current_suite.test_cases[cases - 1].stage_flags |= .Lexer;
} else if arg == "-parse" {
current_suite.test_cases[cases - 1].stage_flags |= .Parser;
} else if arg == "-semant" {
current_suite.test_cases[cases - 1].stage_flags |= .Semantic_Analysis;
} else if arg == "-codegen" {
current_suite.test_cases[cases - 1].stage_flags |= .Codegen;
} else if arg == "-compile" {
current_suite.test_cases[cases - 1].stage_flags |= .Compile;
} else if contains(arg, ".") {
path_split := split(arg, "\\");
split_path := split(path_split[path_split.count - 1], ".");
extension := split_path[1];
if extension == SHADER_EXTENSION {
path := copy_string(arg);
test_case := make_test_case(path, 0);
array_add(*current_suite.test_cases, test_case);
} else {
print("%Invalid file as argument % %\n", red(), arg, reset_color());
}
} else {
print("%Unknown argument % %\n", red(), arg, reset_color());
}
}
case .None; {
if contains(arg, ".") {
path_split := split(arg, "\\");
split_path := split(path_split[path_split.count - 1], ".");
extension := split_path[1];
if extension == SHADER_EXTENSION {
if arg_parse_state == .Run_Suite {
log_error("Unable to run a test while already running suite.");
continue;
}
if !current_suite {
suite : Test_Suite;
array_add(*suites, suite);
current_suite = *suites[0];
}
arg_parse_state = .Run_Test;
path := copy_string(arg);
test_case := make_test_case(path, 0);
array_add(*current_suite.test_cases, test_case);
} else if extension == SUITE_EXTENSION {
if arg_parse_state == .Run_Test {
log_error("Unable to run a suite while already running test.");
continue;
}
arg_parse_state = .Run_Suite;
path := copy_string(arg);
suite : Test_Suite;
read_suite(path, *suite);
array_add(*suites, suite);
current_suite = *suites[0];
} else {
print("%Invalid file as argument % %\n", red(), arg, reset_color());
}
}
}
}
}
for suite : suites {
run_test_suite(*suite, output_type);
}
}