aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authordam <dam@gudinoff>2024-05-18 01:30:01 +0100
committerdam <dam@gudinoff>2024-05-18 01:30:01 +0100
commitaf1317ed93f7d5fc16baceaadb0da17aa17591be (patch)
tree17b0878b430688da9f3481ac621f581d18cbf076 /modules
parentbdea73c8349c2c918befad4120f49f9826d270dc (diff)
downloadtask-time-tracker-af1317ed93f7d5fc16baceaadb0da17aa17591be.tar.zst
task-time-tracker-af1317ed93f7d5fc16baceaadb0da17aa17591be.zip
Updated modules.
Diffstat (limited to 'modules')
-rw-r--r--modules/Saturation/module.jai26
-rw-r--r--modules/Saturation/tests.jai397
-rw-r--r--modules/TUI/module.jai17
-rw-r--r--modules/TUI/snake.jai165
-rw-r--r--modules/UTF8/module.jai29
-rw-r--r--modules/UTF8/tests.jai159
6 files changed, 747 insertions, 46 deletions
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");
}