From af1317ed93f7d5fc16baceaadb0da17aa17591be Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 18 May 2024 01:30:01 +0100 Subject: Updated modules. --- modules/Saturation/module.jai | 26 +-- modules/Saturation/tests.jai | 397 +++++++++++++++++++++++++++++++++++++++--- modules/TUI/module.jai | 17 +- modules/TUI/snake.jai | 165 ++++++++++++++++++ modules/UTF8/module.jai | 29 ++- modules/UTF8/tests.jai | 159 ++++++++++++++++- 6 files changed, 747 insertions(+), 46 deletions(-) create mode 100644 modules/TUI/snake.jai (limited to 'modules') diff --git a/modules/Saturation/module.jai b/modules/Saturation/module.jai index 74643e0..50c9b3c 100644 --- a/modules/Saturation/module.jai +++ b/modules/Saturation/module.jai @@ -1,5 +1,7 @@ // Integer saturating arighmetic (with assembly branch-free procedures for x64 - expecting signed values in two's complement). +#module_parameters(PREFER_BRANCH_FREE_CODE := false); + #import "Basic"; #import "Math"; #import "String"; @@ -37,11 +39,11 @@ INTEGER_ARITHMETIC_TYPES_CHECK :: #string DONE return true; DONE -add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +add :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { + + #if !(prefer_branch_free_code && CPU == .X64) { - #if USE_GENERIC || CPU != .X64 { - #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; } @@ -66,7 +68,7 @@ add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b return x + y, false; } else { - + result: Tr = ---; saturated: bool = ---; @@ -118,10 +120,10 @@ add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } } -sub :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +sub :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { - #if USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -195,10 +197,10 @@ sub :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } -mul :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +mul :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { - #if USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -286,10 +288,10 @@ mul :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } } -div :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, remainder: Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +div :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, remainder: Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { - #if USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -380,11 +382,11 @@ div :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, remainder: T #asm { result === a; // Pin result to register A. remainder === d; // Pin remainder to register D. - + xor result, result; // Clear result. xor remainder, remainder; // Clear remainder (required when used as dividend's high bits). xor saturated, saturated; // Clear saturated (unsigned division never saturates). - mov result, x; // Copy x value to result. + mov.SIZE result, x; // Copy x value to result. DIVIDE_PLACEHOLDER } diff --git a/modules/Saturation/tests.jai b/modules/Saturation/tests.jai index 372d66d..2a82300 100644 --- a/modules/Saturation/tests.jai +++ b/modules/Saturation/tests.jai @@ -1,5 +1,7 @@ // Tests for integer saturating arighmetic procedures. +AVOID_INFINITE_FOR_LOOP :: true; + #import "Basic"; #import "Compiler"; #import "Math"; @@ -12,36 +14,41 @@ main :: () { "#=======================#\n", "# Basic tests #\n" ); + + + test_op :: ($operation: string, x: $Tx, y: $Ty, result: $Tr, type: Type, saturated: bool, remainder: Tr = 0) -> errors_found: int #expand { + + #insert #run () -> string { + // Build test call. + builder: String_Builder; + call := ifx operation == "div" + then "t_result, t_remainder, t_saturated := %(cast(Tx)x, cast(Ty)y);" + else "t_result, t_saturated := %(cast(Tx)x, cast(Ty)y);"; + + print(*builder, call, operation); - test_op :: (operation: string, x: $Tx, y: $Ty, result: $Tr, type: Type, saturated: bool, remainder: Tr = 0) -> errors_found: int #expand { - - print_test_call :: (operation: string) -> string { - str: string = ---; - if operation != "div" { - TEST_CALL :: #string DONE - t_result, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } else { - TEST_CALL :: #string DONE - t_result, t_remainder, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } - return str; - } - - #insert #run print_test_call(operation); + return builder_to_string(*builder); + }(); errors := 0; - if result != t_result { errors += 1; print(" > incorrect result value: got % expected %\n", t_result, result); }; - if type != type_of(t_result) { errors += 1; print(" > incorrect result type: got % expected %\n", type_of(t_result), type); }; - if saturated != t_saturated { errors += 1; print(" > incorrect saturated flag: got % expected %\n", t_saturated, saturated); }; + log: String_Builder; + if result != t_result { errors += 1; print(*log, " > incorrect result value: got % expected %\n", t_result, result); }; + if type != type_of(t_result) { errors += 1; print(*log, " > incorrect result type: got % expected %\n", type_of(t_result), type); }; + if saturated != t_saturated { errors += 1; print(*log, " > incorrect saturated flag: got % expected %\n", t_saturated, saturated); }; #if operation == "div" { - if remainder != t_remainder { errors += 1; print(" > incorrect remainder value: got % expected %\n", t_remainder, remainder); }; + if remainder != t_remainder { errors += 1; print(*log, " > incorrect remainder value: got % expected %\n", t_remainder, remainder); }; } + + if errors > 0 { + #if operation == "div" { + print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated"); + } + else { + print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated"); + } + write_builder(*log); + } + return errors; } @@ -157,6 +164,342 @@ main :: () { if errors > 0 print("# Found % %!\n", errors, ifx errors == 1 then "error" else "errors"); else print(" No errors found.\n"); + + // Test generic agains branch-free alternative. + write_strings( + "#=======================#\n", + "# generic == x64 asm ? #\n" + ); + + + full_test :: ($type: Type, test: (a: type, b: type)) { + #if type == { + case u8; + min :u8 = 0; + max :u8 = U8_MAX; + + case u16; + min :u16 = 0; + max :u16 = U16_MAX; + + case s8; + min :s8 = S8_MIN; + max :s8 = S8_MAX; + + case s16; + min :s16 = S16_MIN; + max :s16 = S16_MAX; + + case; + assert(false, "This will take way too long."); + } + + #if !AVOID_INFINITE_FOR_LOOP { + for a : min..max { + for b : min..max { + test(a, b); + } + } + } + else { + a :type = min; + b :type = min; + while loop_a := true { + while loop_b := true { + test(a, b); + if b == max then break loop_b; else b += 1; + } + if a == max then break loop_a; else a += 1; + } + } + } + + partial_test :: ($type: Type, test: (a: type, b: type)) { + min, max: type; + #if type == { + case u8; + min = 0; + max = U8_MAX; + + case u16; + min = 0; + max = U16_MAX; + + case u32; + min = 0; + max = U32_MAX; + + case s8; + min = S8_MIN; + max = S8_MAX; + + case s16; + min = S16_MIN; + max = S16_MAX; + + case s32; + min = S32_MIN; + max = S32_MAX; + + case; + assert(false, "This will take way too long."); + } + + #if !AVOID_INFINITE_FOR_LOOP { + for a: min..max { + b := a; + c := max - a + min; + test(a, b); + test(a, c); + } + } + else { + a := min; + while loop := true { + b := a; + c := max - a + min; + test(a, b); + test(a, c); + if a == max then break loop; else a += 1; + } + } + } + + minimal_test :: ($type: Type, test: (a: type, b: type)) { + #if type == { + case u32; + min :u32 = 0; + mid :u32 = U32_MAX / 2; + max :u32 = U32_MAX; + range :u32 = cast(u32)U16_MAX * 2048; + + case u64; + min :u64 = 0; + mid :u64 = U64_MAX / 2; + max :u64 = U64_MAX; + range :u64 = cast(u64)U16_MAX * 2048; + + case s32; + min :s32 = S32_MIN; + mid :s32 = (S32_MIN / 2) + (S32_MAX / 2); + max :s32 = S32_MAX; + range :s32 = cast(s32)S16_MAX * 2048; + + case s64; + min :s64 = S64_MIN; + mid :s64 = (S64_MIN / 2) + (S64_MAX / 2); + max :s64 = S64_MAX; + range :s64 = cast(s64)S16_MAX * 2048; + + case; + assert(false, "Invalid type % given.", type); + } + + #if !AVOID_INFINITE_FOR_LOOP { + start, end : type; + + start = min; + end = min+range; + for a: start..end { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + } + + start = mid-range; + end = mid+range; + for a: start..end { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + } + + start = max-range; + end = max; + for a: start..end { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + } + } + else { + start, end, a : type; + + start = min; + end = min + range; + a = start; + while loop := true { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + if a == end then break loop; else a += 1; + } + + start = mid - range; + end = mid + range; + a = start; + while loop := true { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + if a == end then break loop; else a += 1; + } + + start = max - range; + end = max; + a = start; + while loop := true { + b := a; + c := end - a + start; + test(a, b); + test(a, c); + if a == end then break loop; else a += 1; + } + } + } + + + // add + + ADD_TEST_TEMPLATE :: (a: $T, b: T) { + rT, sT := add(a, b, true); + rF, sF := add(a, b, false); + assert(rT == rF && sT == sF, "> add(%1, %2, true) = %3,%4 != add(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF); + } + + write_string("# testing add,u8 #\n"); + full_test(u8, ADD_TEST_TEMPLATE); + + write_string("# testing add,u16 #\n"); + full_test(u16, ADD_TEST_TEMPLATE); + + write_string("# testing add,u32 #\n"); + partial_test(u32, ADD_TEST_TEMPLATE); + + write_string("# testing add,u64 #\n"); + minimal_test(u64, ADD_TEST_TEMPLATE); + + write_string("# testing add,s8 #\n"); + full_test(s8, ADD_TEST_TEMPLATE); + + write_string("# testing add,s16 #\n"); + full_test(s16, ADD_TEST_TEMPLATE); + + write_string("# testing add,s32 #\n"); + partial_test(s32, ADD_TEST_TEMPLATE); + + write_string("# testing add,s64 #\n"); + minimal_test(s64, ADD_TEST_TEMPLATE); + + + // sub + + SUB_TEST_TEMPLATE :: (a: $T, b: T) { + rT, sT := sub(a, b, true); + rF, sF := sub(a, b, false); + assert(rT == rF && sT == sF, "> sub(%1, %2, true) = %3,%4 != sub(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF); + } + + write_string("# testing sub,u8 #\n"); + full_test(u8, SUB_TEST_TEMPLATE); + + write_string("# testing sub,u16 #\n"); + full_test(u16, SUB_TEST_TEMPLATE); + + write_string("# testing sub,u32 #\n"); + partial_test(u32, SUB_TEST_TEMPLATE); + + write_string("# testing sub,u64 #\n"); + minimal_test(u64, SUB_TEST_TEMPLATE); + + write_string("# testing sub,s8 #\n"); + full_test(s8, SUB_TEST_TEMPLATE); + + write_string("# testing sub,s16 #\n"); + full_test(s16, SUB_TEST_TEMPLATE); + + write_string("# testing sub,s32 #\n"); + partial_test(s32, SUB_TEST_TEMPLATE); + + write_string("# testing sub,s64 #\n"); + minimal_test(s64, SUB_TEST_TEMPLATE); + + + // mul + + MUL_TEST_TEMPLATE :: (a: $T, b: T) { + rT, sT := mul(a, b, true); + rF, sF := mul(a, b, false); + assert(rT == rF && sT == sF, "> mul(%1, %2, true) = %3,%4 != mul(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF); + } + + write_string("# testing mul,u8 #\n"); + full_test(u8, MUL_TEST_TEMPLATE); + + write_string("# testing mul,u16 #\n"); + full_test(u16, MUL_TEST_TEMPLATE); + + write_string("# testing mul,u32 #\n"); + partial_test(u32, MUL_TEST_TEMPLATE); + + write_string("# testing mul,u64 #\n"); + minimal_test(u64, MUL_TEST_TEMPLATE); + + write_string("# testing mul,s8 #\n"); + full_test(s8, MUL_TEST_TEMPLATE); + + write_string("# testing mul,s16 #\n"); + full_test(s16, MUL_TEST_TEMPLATE); + + write_string("# testing mul,s32 #\n"); + partial_test(s32, MUL_TEST_TEMPLATE); + + write_string("# testing mul,s64 #\n"); + minimal_test(s64, MUL_TEST_TEMPLATE); + + + // div + + DIV_TEST_TEMPLATE :: (a: $T, b: T) { + if b == 0 then return; + rT, remT, sT := div(a, b, true); + rF, remF, sF := div(a, b, false); + assert(rT == rF && sT == sF, "> mul(%1, %2, true) = %3,%4,%5 != mul(%1, %2, false) = %6,%7,%8\n", a, b, rT, remT, sT, rF, remF, sF); + } + + write_string("# testing div,u8 #\n"); + full_test(u8, DIV_TEST_TEMPLATE); + + write_string("# testing div,u16 #\n"); + full_test(u16, DIV_TEST_TEMPLATE); + + write_string("# testing div,u32 #\n"); + partial_test(u32, DIV_TEST_TEMPLATE); + + write_string("# testing div,u64 #\n"); + minimal_test(u64, DIV_TEST_TEMPLATE); + + write_string("# testing div,s8 #\n"); + full_test(s8, DIV_TEST_TEMPLATE); + + write_string("# testing div,s16 #\n"); + full_test(s16, DIV_TEST_TEMPLATE); + + write_string("# testing div,s32 #\n"); + partial_test(s32, DIV_TEST_TEMPLATE); + + write_string("# testing div,s64 #\n"); + minimal_test(s64, DIV_TEST_TEMPLATE); + + + write_string(" No errors found.\n"); + + write_strings( "#=======================#\n", "# Benchmarks #\n" @@ -212,11 +555,11 @@ main :: () { r_asm: type = 0; time_gen := current_time_monotonic(); - for idx: 0..DATA_SIZE-1 #insert #run replace("r_gen ^= OP(numbers_x[idx], numbers_y[idx], true);", "OP", operation); + for idx: 0..DATA_SIZE-1 #insert #run replace("r_gen ^= OP(numbers_x[idx], numbers_y[idx], false);", "OP", operation); time_gen = current_time_monotonic() - time_gen; time_asm := current_time_monotonic(); - for idx: 0..DATA_SIZE-1 #insert #run replace("r_asm ^= OP(numbers_x[idx], numbers_y[idx]);", "OP", operation); + for idx: 0..DATA_SIZE-1 #insert #run replace("r_asm ^= OP(numbers_x[idx], numbers_y[idx], true);", "OP", operation); time_asm = current_time_monotonic() - time_asm; assert(r_gen == r_asm); diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index fb742da..d0373f9 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,3 +1,16 @@ +/* + A simple terminal user interface module that provides basic functionalities similar to the [ncurses library](https://en.wikipedia.org/wiki/Ncurses). + Usefull for creating simple terminal-based apps that require user input. + View `snake.jai` for an example. + It has been tested on the following terminal emulators: + - [GNOME Terminal](https://en.wikipedia.org/wiki/GNOME_Terminal) + - [kitty](https://en.wikipedia.org/wiki/Kitty_(terminal_emulator)) + - [Konsole](https://en.wikipedia.org/wiki/Konsole) + - [Linux console](https://en.wikipedia.org/wiki/Linux_console) + - [xterm](https://en.wikipedia.org/wiki/Xterm) + - [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal) +*/ + #module_parameters(COLOR_MODE_BITS := 24); @@ -32,11 +45,11 @@ #import "UTF8"; #load "key_map.jai"; -#add_context tui_style : Style; // This contains the last style applied by the module. +#add_context tui_style : Style; // This contains the last style applied by the module. #add_context tui_output_builder : *String_Builder; // If set, this will serve as an output buffer for this module procedures. KEY_SIZE :: #run type_info(Key).runtime_size; -#assert(input_buffer.count >= KEY_SIZE); // The input buffer size must be capable to hold an entire Key. +#assert(input_buffer.count >= KEY_SIZE); // The input buffer size must be capable to hold an entire Key. active := false; input_override : Key; diff --git a/modules/TUI/snake.jai b/modules/TUI/snake.jai new file mode 100644 index 0000000..b35c0fb --- /dev/null +++ b/modules/TUI/snake.jai @@ -0,0 +1,165 @@ +#import "Basic"; +#import "Random"; +TUI :: #import "TUI"(COLOR_MODE_BITS = 8); + +Vec2D :: struct { + x: int; + y: int; +} + +operator == :: (a: Vec2D, b: Vec2D) -> bool { + return a.x == b.x && a.y == b.y; +} + +screen_size_x: int = ---; +screen_size_y: int = ---; +player_name: string = ---; + +main :: () { + + game_loop :: () { + + LOOP_PERIOD_MS :: 30; + + score := 0; + dir := Vec2D.{1, 0}; + food := Vec2D.{5, 5}; + + snake_parts: [..] Vec2D; + for 0..13 array_add(*snake_parts, Vec2D.{3, 3}); + snake_parts[0].x += 1; + + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.Resize); + timer := current_time_monotonic(); + while main_loop := true { + + timestamp := current_time_monotonic(); + key := TUI.get_key(LOOP_PERIOD_MS); + + if key == { + case TUI.Keys.Resize; + TUI.clear_terminal(); + screen_size_x, screen_size_y = TUI.get_terminal_size(); + TUI.draw_box(1, 1, screen_size_x, screen_size_y); + TUI.set_cursor_position(3, screen_size_y); + write_strings(" ", player_name, " "); + + case #char "q"; #through; + case #char "Q"; #through; + case TUI.Keys.Escape; + break main_loop; + + case TUI.Keys.Up; + if dir != Vec2D.{0, 1} then dir = Vec2D.{0, -1}; + + case TUI.Keys.Down; + if dir != Vec2D.{0, -1} then dir = Vec2D.{0, 1}; + + case TUI.Keys.Left; + if dir != Vec2D.{1, 0} then dir = Vec2D.{-1, 0}; + + case TUI.Keys.Right; + if dir != Vec2D.{-1, 0} then dir = Vec2D.{1, 0}; + } + + if screen_size_x < 15 || screen_size_y < 15 { + TUI.clear_terminal(); + TUI.set_cursor_position(1,1); + write_string("~ paused : increase window size ~"); + continue; + } + + last_pos := snake_parts[snake_parts.count-1]; + + // Update position. + for < snake_parts.count-1..1 { + if snake_parts[it] != snake_parts[it-1] { + snake_parts[it] = snake_parts[it-1]; + } + } + snake_parts[0].x += dir.x; + snake_parts[0].y += dir.y; + + // Teleport on borders. + if snake_parts[0].x < 2 then snake_parts[0].x = screen_size_x - 1; + if snake_parts[0].x >= screen_size_x then snake_parts[0].x = 2; + if snake_parts[0].y < 2 then snake_parts[0].y = screen_size_y - 1; + if snake_parts[0].y >= screen_size_y then snake_parts[0].y = 2; + food.x = clamp(food.x, 2, screen_size_x-1); + food.y = clamp(food.y, 2, screen_size_y-1); + + // Check for game-over. + for 1..snake_parts.count-1 { + if snake_parts[it] == snake_parts[0] { + break main_loop; + } + } + + // Check for food. + if snake_parts[0] == food { + score += 1; + array_add(*snake_parts, snake_parts[snake_parts.count-1]); + food = Vec2D.{ + cast(int)(random_get_zero_to_one_open() * (screen_size_x-3) + 2), + cast(int)(random_get_zero_to_one_open() * (screen_size_y-3) + 2) + }; + } + + // Wait to match game loop time. + delta := to_milliseconds(current_time_monotonic() - timestamp); + if delta < LOOP_PERIOD_MS { + sleep_milliseconds(xx (LOOP_PERIOD_MS - delta)); + } + + // Draw snake. + write_string(TUI.Commands.DrawingMode); + TUI.set_cursor_position(last_pos.x, last_pos.y); + write_string(TUI.Drawings.Blank); + for snake_parts { + TUI.set_cursor_position(it.x, it.y); + write_string(TUI.Drawings.Checkerboard); + } + // Draw food. + { + TUI.using_style(TUI.Style.{ foreground = TUI.Palette.RED, bold = true, }); + TUI.set_cursor_position(food.x, food.y); + write_string(TUI.Drawings.Diamond); + } + write_string(TUI.Commands.TextMode); + + // Set score + TUI.set_cursor_position(3, 1); + print(" % ", score); + } + } + + GAME_OVER_TEXT :: "~ game over ~"; + INSTRUCTIONS_TEXT :: "(esc to exit)"; + + seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. + random_seed(seed); + + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.set_cursor_position(1, 1); + + write_string("Please enter player name: "); + player_name = TUI.read_input_line(64); + + while true { + game_loop(); + + // Game over screen. + box_size := Vec2D.{19, 4}; + TUI.draw_box((screen_size_x-box_size.x)/2, (screen_size_y-box_size.y)/2, box_size.x, box_size.y); + TUI.set_cursor_position((screen_size_x-GAME_OVER_TEXT.count)/2, (screen_size_y-box_size.y)/2 + 1); + write_string(GAME_OVER_TEXT); + TUI.set_cursor_position((screen_size_x-GAME_OVER_TEXT.count)/2, (screen_size_y-box_size.y)/2 + 2); + write_string(INSTRUCTIONS_TEXT); + sleep_milliseconds(100); + TUI.flush_input(); + if TUI.get_key() == TUI.Keys.Escape then break; + } + + assert(TUI.reset_terminal(), "Failed to reset TUI."); +} diff --git a/modules/UTF8/module.jai b/modules/UTF8/module.jai index 72d3d75..5e6fd65 100644 --- a/modules/UTF8/module.jai +++ b/modules/UTF8/module.jai @@ -8,15 +8,15 @@ is_continuation_byte :: inline (byte: u8) -> bool { } // Given a leading_byte, returns the number of bytes on the character. -count_character_bytes :: inline (leading_byte: u8) -> int { +count_character_bytes :: inline (character_leading_byte: u8) -> int { // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte - if (leading_byte & 0xE0) == 0xC0 return 1+1; + if (character_leading_byte & 0xE0) == 0xC0 return 1+1; // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte - if (leading_byte & 0xF0) == 0xE0 return 1+2; + if (character_leading_byte & 0xF0) == 0xE0 return 1+2; // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte - if (leading_byte & 0xF8) == 0xF0 return 1+3; + if (character_leading_byte & 0xF8) == 0xF0 return 1+3; return 1; } @@ -126,3 +126,24 @@ get_byte_index :: (str: string, character_index: int) -> buffer_index: int, succ } return -1, false; } + +// Scans the string for UTF8 encoding errors. +is_valid :: (str: string) -> is_valid := true, error_index: int = -1 { + idx := 0; + remainig_bytes := 0; + while idx < str.count { + defer idx += 1; + + is_continuation := is_continuation_byte(str[idx]); + + if (is_continuation && remainig_bytes == 0) || (!is_continuation && remainig_bytes > 0) then return false, idx; + + if is_continuation { + remainig_bytes -= 1; + continue; + } + + remainig_bytes = count_character_bytes(str[idx]) - 1; + } + return; +} diff --git a/modules/UTF8/tests.jai b/modules/UTF8/tests.jai index aff1d36..b7e3579 100644 --- a/modules/UTF8/tests.jai +++ b/modules/UTF8/tests.jai @@ -1,5 +1,162 @@ #import "Basic"; +#import "String"; +#import "UTF8"; main :: () { - assert(false, "TODO"); // TODO + write_strings( + "#=======================#\n", + "# Basic tests #\n" + ); + + tmp_str: string; + tmp_bool: bool; + tmp_int: int; + + assert(is_continuation_byte("0€1"[0]) == false); + assert(is_continuation_byte("0€1"[1]) == false); + assert(is_continuation_byte("0€1"[2]) == true); + assert(is_continuation_byte("0€1"[3]) == true); + assert(is_continuation_byte("0€1"[4]) == false); + + + write_strings("# count_character_bytes #\n"); + + assert(count_character_bytes("0£€𐍈1"[0]) == 1); + assert(count_character_bytes("0£€𐍈1"[1]) == 2); + assert(count_character_bytes("0£€𐍈1"[2]) == 1); + assert(count_character_bytes("0£€𐍈1"[3]) == 3); + assert(count_character_bytes("0£€𐍈1"[4]) == 1); + assert(count_character_bytes("0£€𐍈1"[5]) == 1); + assert(count_character_bytes("0£€𐍈1"[6]) == 4); + assert(count_character_bytes("0£€𐍈1"[7]) == 1); + assert(count_character_bytes("0£€𐍈1"[8]) == 1); + assert(count_character_bytes("0£€𐍈1"[9]) == 1); + assert(count_character_bytes("0£€𐍈1"[10]) == 1); + + + write_strings("# truncate #\n"); + + assert(compare(truncate("0£€𐍈1", 0), "") == 0); + assert(compare(truncate("0£€𐍈1", 1), "0") == 0); + assert(compare(truncate("0£€𐍈1", 2), "0") == 0); + assert(compare(truncate("0£€𐍈1", 3), "0£") == 0); + assert(compare(truncate("0£€𐍈1", 4), "0£") == 0); + assert(compare(truncate("0£€𐍈1", 5), "0£") == 0); + assert(compare(truncate("0£€𐍈1", 6), "0£€") == 0); + assert(compare(truncate("0£€𐍈1", 7), "0£€") == 0); + assert(compare(truncate("0£€𐍈1", 8), "0£€") == 0); + assert(compare(truncate("0£€𐍈1", 9), "0£€") == 0); + assert(compare(truncate("0£€𐍈1", 10), "0£€𐍈") == 0); + assert(compare(truncate("0£€𐍈1", 11), "0£€𐍈1") == 0); + assert(compare(truncate("0£€𐍈1", 12), "0£€𐍈1") == 0); + + + write_strings("# is_empty #\n"); + + assert(is_empty("")); + assert(is_empty("\0")); + assert(is_empty("\0\t")); + assert(is_empty("\0\t\n")); + assert(is_empty("\0\t\n\x0B")); + assert(is_empty("\0\t\n\x0B\x0C")); + assert(is_empty("\0\t\n\x0B\x0C\r")); + assert(is_empty("\0\t\n\x0B\x0C\r ")); + assert(is_empty("\0\t\n\x0B\x0C\r .") == false); + assert(is_empty("| B A Z € N G A |") == false); + + + write_strings("# delete_character #\n"); + + tmp_str = copy_string("",, temporary_allocator); + assert(delete_character(*tmp_str, 0) == false); + + tmp_str = copy_string("12£45€78𐍈",, temporary_allocator); + assert(delete_character(*tmp_str, -1) == false); + assert(delete_character(*tmp_str, 99999) == false); + assert(delete_character(*tmp_str, 7) == true); + assert(compare(tmp_str, "12£45€7𐍈") == 0); + assert(delete_character(*tmp_str, 2) == true); + assert(compare(tmp_str, "1245€7𐍈") == 0); + assert(delete_character(*tmp_str, 4) == true); + assert(compare(tmp_str, "12457𐍈") == 0); + assert(delete_character(*tmp_str, 3) == true); + assert(compare(tmp_str, "1247𐍈") == 0); + + + write_strings("# get_byte_index #\n"); + + tmp_str = copy_string("12£45€78𐍈X",, temporary_allocator); + + tmp_int, tmp_bool = get_byte_index("", 0); + assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, -1); + assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, -99999); + assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 99999); + assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool); + + tmp_int, tmp_bool = get_byte_index(tmp_str, 0); + assert(tmp_int == 0 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 1); + assert(tmp_int == 1 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 2); + assert(tmp_int == 2 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 3); + assert(tmp_int == 4 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 4); + assert(tmp_int == 5 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 5); + assert(tmp_int == 6 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 6); + assert(tmp_int == 9 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 7); + assert(tmp_int == 10 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 8); + assert(tmp_int == 11 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + tmp_int, tmp_bool = get_byte_index(tmp_str, 9); + assert(tmp_int == 15 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool); + + + write_strings("# count_characters #\n"); + + assert(count_characters("") == 0); + assert(count_characters("0") == 1); + assert(count_characters("0£") == 2); + assert(count_characters("0£€") == 3); + assert(count_characters("0£€𐍈") == 4); + assert(count_characters("0£€𐍈1") == 5); + + tmp_str = copy_string("123€DELETE",, temporary_allocator); + tmp_str[6] = 0; + assert(count_characters(tmp_str) == 10); + assert(count_characters(tmp_str, true) == 4); + + + write_strings("# is_valid #\n"); + + assert(is_valid("")); + + tmp_str = copy_string("123€DELETE",, temporary_allocator); + tmp_str[6] = 0; + tmp_bool, tmp_int = is_valid(tmp_str); + assert(tmp_bool == true && tmp_int == -1, "(%, %)", tmp_bool, tmp_int); + + tmp_str = copy_string("123€DELETE",, temporary_allocator); + tmp_str[3] = 0; // Cut € at start. + tmp_bool, tmp_int = is_valid(tmp_str); + assert(tmp_bool == false && tmp_int == 4, "(%, %)", tmp_bool, tmp_int); + + tmp_str = copy_string("123€DELETE",, temporary_allocator); + tmp_str[4] = 0; // Cut € at middle. + tmp_bool, tmp_int = is_valid(tmp_str); + assert(tmp_bool == false && tmp_int == 4, "(%, %)", tmp_bool, tmp_int); + + tmp_str = copy_string("123€DELETE",, temporary_allocator); + tmp_str[5] = 0; // Cut € at end. + tmp_bool, tmp_int = is_valid(tmp_str); + assert(tmp_bool == false && tmp_int == 5, "(%, %)", tmp_bool, tmp_int); + + + write_strings(" No errors found.\n"); } -- cgit v1.2.3