From c42131d90b1886e0f7704b50b8858160b68514bc Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Sun, 11 May 2025 02:29:36 -0600 Subject: [PATCH] init --- .gitignore | 1 + array.jai | 96 +++++++++++++++++ encoding/base64.jai | 247 ++++++++++++++++++++++++++++++++++++++++++++ encoding/module.jai | 4 + macros.jai | 126 ++++++++++++++++++++++ memory.jai | 32 ++++++ module.jai | 26 +++++ test/module.jai | 48 +++++++++ utils.jai | 42 ++++++++ 9 files changed, 622 insertions(+) create mode 100644 .gitignore create mode 100644 array.jai create mode 100644 encoding/base64.jai create mode 100644 encoding/module.jai create mode 100644 macros.jai create mode 100644 memory.jai create mode 100644 module.jai create mode 100644 test/module.jai create mode 100644 utils.jai diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/array.jai b/array.jai new file mode 100644 index 0000000..00f1c51 --- /dev/null +++ b/array.jai @@ -0,0 +1,96 @@ +Static_Array :: struct(capacity: int, T: Type) { + items: [capacity]T; + count: int; + + Zero :: #run zero_value(T); +} + +operator [] :: inline (a: Static_Array, index: int, loc := #caller_location) -> a.T #no_abc { + assert(index >= 0 && index < a.count, "invalid index: % (max: %)", index, a.count - 1, loc = loc); + return a.items[index]; +} + +operator *[] :: inline (a: *Static_Array, index: int, loc := #caller_location) -> *a.T #no_abc { + assert(index >= 0 && index < a.count, "invalid index: % (max: %)", index, a.count - 1, loc = loc); + return *a.items[index]; +} + +operator []= :: inline (a: *Static_Array, index: int, value: a.T, loc := #caller_location) #no_abc { + assert(index >= 0 && index < a.capacity, "invalid index: % (max: %)", index, a.count - 1, loc = loc); + a.items[index] = value; +} + +for_expansion :: (a: *Static_Array, body: Code, flags: For_Flags) #expand { + view := make_view(a); + for *=(flags & .POINTER == .POINTEr) <=(flags & .REVERSE == .REVERSE) `it, `it_index: iter { + #insert,scope(body)(break = break it) body; + } +} + +append :: inline (a: *Static_Array, item: a.T) -> *a.T { + ensure_array_has_room(a, 1); + ptr := *a[a.count]; + ptr.* = item; + a.count += 1; + return ptr; +} + +append :: inline (a: *Static_Array) -> *a.T { + ensure_array_has_room(a, 1); + ptr := *a[a.count]; + a.count += 1; + return ptr; +} + +append :: inline (a: *Static_Array, items: ..a.T) -> *a.T { + ensure_array_has_room(a, items.count); + first := *a.items[a.count]; + memcpy(a.items.data + a.count, items.data, items.count * size_of(a.T)); + a.count += items.count; + return first; +} + +reset :: inline (a: *Static_Array, $keep_memory := true) { + #if keep_memory { + a.count = 0; + } + else { + for 0..a.count - 1 a.items[it] = a.Zero; + a.count = 0; + } +} + +make_view :: (a: Static_Array) -> []a.T { + view := a.items.([]a.T); + view.count = a.count; + return view; +} + +make_dynamic :: (a: *Static_Array) -> [..]a.T { + res: [..]a.T; + res.count = a.count; + res.allocated = a.count; + res.data = basic.alloc(a.count * size_of(a.T)); + memcpy(res.data, a.items.data, a.count * size_of(a.T)); +} + + +#scope_file; + +ensure_array_has_room :: (array: *Static_Array, count: int, loc := #caller_location) #expand { + basic.assert(array.count + count <= array.capacity, "attempt to add too many elements! want: %, Max: %", array.count + count, array.capacity, loc = loc); +} + +#if RUN_TESTS #run { + test :: #import,file "./test/module.jai"; + + test.run("basic operations", (t) => { + a: Static_Array(10, int); + + test.expect(t, a.count == 0); + test.expect(t, a.capacity == 10); + + append(*a, 10, 20, 30); + test.expect(t, a.count == 3, "count: %", a.count); + }); +} diff --git a/encoding/base64.jai b/encoding/base64.jai new file mode 100644 index 0000000..32fbd3c --- /dev/null +++ b/encoding/base64.jai @@ -0,0 +1,247 @@ +base64_encode :: (str: string, $for_url := false) -> string, bool { + enc, ok := base64_encode(str.([]u8), for_url); + return enc.(string), ok; +} + +base64_decode :: (str: string) -> string, bool { + enc, ok := base64_decode(str.([]u8)); + return enc.(string), ok; +} + +base64_encode :: ($$data: []u8, $for_url := false) -> []u8, bool { + if data.count == 0 return .[], false; + + #if for_url { + Character_Set :: Encoding_Url; + } + else { + Character_Set :: Encoding_Standard; + } + + padded := strings.contains(data.(string), Padding_Character); + encoded := basic.NewArray(encoded_length(data.count, padded), u8, true); + + src_idx := 0; + dst_idx := 0; + src_max := (data.count / 3) * 3; + while src_idx < src_max { + value: u64; + + parts := (*value).(*u8); + (parts + 0).* = xx data[src_idx + 2]; + (parts + 1).* = xx data[src_idx + 1]; + (parts + 2).* = xx data[src_idx + 0]; + + encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F]; + encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F]; + encoded[dst_idx + 2] = Character_Set[value >> 6 & 0x3F]; + encoded[dst_idx + 3] = Character_Set[value & 0x3F]; + + src_idx += 3; + dst_idx += 4; + } + + remaining := data.count - src_idx; + if !remaining then return encoded, true; + + value := data[src_idx + 0].(u64) << 16; + if remaining == 2 { + value |= data[src_idx + 1].(u64) << 8; + } + + encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F]; + encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F]; + + if remaining == { + case 2; + if padded { + encoded[dst_idx + 2] = Padding_Character; + encoded[dst_idx + 3] = Padding_Character; + } + case 1; + encoded[dst_idx + 2] = Character_Set[value >> 6 & 0x3F]; + if padded { + encoded[dst_idx + 3] = Padding_Character; + } + } + + return encoded, true; +} + +base64_decode :: (data: []u8) -> []u8, bool { + if !data.count return .[], false; + + lookup :: (c: u8) -> u8 #expand { + if c >= #char "A" && c <= #char "Z" return c - #char "A"; + if c >= #char "a" && c <= #char "z" return c - 71; + if c >= #char "0" && c <= #char "9" return c + 4; + if c == #char "+" || c == #char "-" return 62; + if c == #char "/" || c == #char "_" return 63; + return 255; + } + + a4_to_a3 :: () #expand { + `a3[0] = (`a4[0] << 2) + ((`a4[1] & 0x30) >> 4); + `a3[1] = ((`a4[1] & 0xF) << 4) + ((`a4[2] & 0x3C) >> 2); + `a3[2] = ((`a4[2] & 0x3) << 6) + `a4[3]; + } + + padding := 0; + { + i := data.count - 1; + while i >= 0 { + if data[i] == Padding_Character { + padding += 1; + } + else { + break; + } + + i -= 1; + } + } + + a3: [3]u8; + a4: [4]u8; + + i := 0; + offset := 0; + + decoded := basic.NewArray(((6 * data.count) / 8) - padding, u8, true); + for 0..data.count - 1 { + chr := data[it]; + if chr == Padding_Character break; + + a4[i] = chr; + i += 1; + + if i == 4 { + for 0..3 a4[it] = lookup(a4[it]); + + a4_to_a3(); + + for 0..2 { + decoded.data[offset] = a3[it]; + offset += 1; + } + + i = 0; + } + } + + if i > 0 { + for 0..3 a4[it] = lookup(a4[it]); + + a4_to_a3(); + + for 0..i - 1 { + (decoded.data + offset + it).* = a3[it]; + } + } + + return decoded, true; +} + +#scope_file; + +Padding_Character :: #char "="; +Encoding_Url :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +Encoding_Standard :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +encoded_length :: (count: int, with_padding := false) -> int { + if with_padding then return (count + 2) / 3 * 4; + return (count * 8 + 5) / 6; +} + +basic :: #import "Basic"; +strings :: #import "String"; + +#if RUN_TESTS #run { + test :: #import,file "../test/module.jai"; + + test.run("encodes", (t) => { + str :: "Hello, World"; + + encoded, ok := base64_encode(str); + test.expect(t, ok, "'%' did not properly encode!", str); + + expected :: "SGVsbG8sIFdvcmxk"; + test.expect(t, encoded == expected, "wanted: '%', received: '%'", expected, encoded); + }); + + test.run("decodes", (t) => { + encoded_str :: "SGVsbG8sIFdvcmxk"; + + decoded, ok := base64_decode(encoded_str); + test.expect(t, ok, "'%' did not properly decode!", encoded_str); + + expected :: "Hello, World"; + test.expect(t, decoded == expected, "wanted: '%', received: '%'", expected, decoded); + }); + + test.run("encodes/decodes padding", (t) => { + str :: "VkdocGN5QnBjeUJ6YjIxbGRHaHBibWNnYlc5eVpTQmpiMjF3YkdWNExDQnBkQ0J6YUc5MWJHUWdhR0YyWlNCd1lXUmthVzVuUHc9PQ=="; + + first_decode, f_ok := base64_decode(str); + test.expect(t, f_ok, "first decode failed!"); + + re_encode, r_ok := base64_encode(first_decode); + test.expect(t, r_ok, "re-encode failed!"); + + second_decode, s_ok := base64_decode(re_encode); + test.expect(t, s_ok, "second decode failed!"); + + for 0..first_decode.count - 1 { + test.expect(t, first_decode[it] == second_decode[it], "Decoded byte '%' did not match expected output", it); + } + }); + + test.run("encodes/decodes struct", (t) => { + Foo :: struct { + first: u64; + second: u64; + third: u64; + forth: u64; + } + + foo: Foo; + foo.first = 123; + foo.second = 456; + foo.third = 789; + foo.forth = 100; + + enc_builder: basic.String_Builder; + basic.print_to_builder(*enc_builder, "%,%,%,%", foo.first, foo.second, foo.third, foo.forth); + + encoded, e_ok := base64_encode(basic.builder_to_string(*enc_builder)); + test.expect(t, e_ok, "Encode of structure failed!"); + + decoded, d_ok := base64_decode(encoded); + test.expect(t, d_ok, "Decode of encoded structure failed!"); + + parts := strings.split(decoded, ","); + test.expect(t, parts.count == 4, "Invalid number of parts after decode: %", parts.count); + + bar: Foo = ---; + + { + ptr := (*bar).(*u8); + info := Foo.(*Type_Info_Struct); + + for info.members { + value, ok := strings.parse_int(*parts[it_index], u64); + test.expect(t, ok, "Integer parse of value % ('%') failed!", it_index, parts[it_index]); + + offset := (ptr + it.offset_in_bytes).(*u64); + offset.*= value; + } + } + + test.expect(t, foo.first == bar.first, "Foo.first (%, %) didn't match between encoding/decoding!", foo.first, bar.first); + test.expect(t, foo.second == bar.second, "Foo.second (%, %) didn't match between encoding/decoding!", foo.second, bar.second); + test.expect(t, foo.third == bar.third, "Foo.third (%, %) didn't match between encoding/decoding!", foo.third, bar.third); + test.expect(t, foo.forth == bar.forth, "Foo.forth (%, %) didn't match between encoding/decoding!", foo.forth, bar.forth); + }); +} + + diff --git a/encoding/module.jai b/encoding/module.jai new file mode 100644 index 0000000..50f62a8 --- /dev/null +++ b/encoding/module.jai @@ -0,0 +1,4 @@ +#module_parameters(RUN_TESTS := false); + +#load "base64.jai"; + diff --git a/macros.jai b/macros.jai new file mode 100644 index 0000000..04520aa --- /dev/null +++ b/macros.jai @@ -0,0 +1,126 @@ +verify :: (block: Code) #expand { + for 0..0 #insert,scope(block) block; +} + +c_call :: (block: Code) #expand { + push_context context { + #insert,scope() block; + } +} + +zero_value :: ($T: Type) -> T #expand { + zero: T; + return zero; +} + +// Allows structs to be copy assigned. +with :: (old: $T, $new: Code, location := #caller_location) -> T +#modify { return T.(*Type_Info).type == .STRUCT, "with can only be used on structs"; } +#expand { + using compiler; + + ensure :: (cond: bool, message: string, args: ..Any, loc := location) { + if !cond compiler_report(basic.tprint(message, ..args), loc); + } + + #insert,scope() -> string { + b: basic.String_Builder; + root := compiler_get_nodes(new).(*Code_Literal); + ensure(root.kind == .LITERAL, "argument must be a struct literal"); + ensure(root.value_type == .STRUCT, "argument must be a struct literal"); + + t_info := T.(*Type_Info_Struct); + + // Ensure the literal we were given is of type T so this operator is more predictable. + // i.e. disallowing Vector3.{} | SomeRandomType.{ x = 10 }; + n_type := root.struct_literal_info.type_expression; + if n_type != null { + tmp: basic.String_Builder; + pp.print_expression(*tmp, n_type); + n_typename := basic.builder_to_string(*tmp); + ensure(n_type.result == t_info, "mismatched types, % vs. %", t_info.name, n_typename); + } + + receiver := "old"; + if is_constant(old) { + receiver = "copy"; + basic.print_to_builder(*b, "% := old;\n", receiver); + } + + for root.struct_literal_info.arguments { + op := it.(*Code_Binary_Operator); + ident := op.left.(*Code_Ident); + + ensure(op.kind == .BINARY_OPERATOR, "copy-assign requires named fields", loc = make_location(op)); + ensure(op.operator_type == #char "=", "copy-assign requires named field assignment", loc = make_location(op)); + ensure(ident.kind == .IDENT, "must be an identifier", loc = make_location(op)); + + // Catch any incorrect field assignments before the compiler does for better error reporting + exists := false; + for t_info.members if it.name == ident.name exists = true; + ensure(exists, "field % does not exist within %", ident.name, t_info.name, loc = make_location(ident)); + + // receiver.field = value; + basic.append(*b, receiver); + basic.append(*b, "."); + pp.print_expression(*b, op); + basic.append(*b, ";\n"); + } + + basic.print_to_builder(*b, "return %;\n", receiver); + return basic.builder_to_string(*b); + } +} + +operator | :: with; + + +#scope_file; + +test :: #import,file "test/module.jai"; + +pp :: #import "Program_Print"; +compiler :: #import "Compiler"; + +#if RUN_TESTS #run { + test.run("verify", (t) => { + i := 0; + verify(#code { + i += 1; + if i == 1 { + break; + } + + i += 2; + }); + + j := 0; + verify(#code { + for 0..10 { + break; + } + + j = 1; + }); + + test.expect(t, i == 1, "i was %", i); + test.expect(t, j == 1, "j was %", j); + }); + + test.run("copy assign", (t) => { + Value :: struct { + x: float; + y: float; + z: float; + } + + a := Value.{ 10, 20, 30 }; + b := with(a, .{ x = 1, z = 1 }); + c := b | .{ y = 500 }; + + test.expect(t, b.x == 1, "was %", b.x); + test.expect(t, b.z == 1, "was %", b.z); + test.expect(t, c.y == 500, "was %", c.y); + }); +} + diff --git a/memory.jai b/memory.jai new file mode 100644 index 0000000..136f686 --- /dev/null +++ b/memory.jai @@ -0,0 +1,32 @@ +Kilobyte :: 1024; +Megabyte :: 1024 * Kilobyte; +Gigabyte :: 1024 * Megabyte; + +Default_Align :: #run 2 * align_of(rawptr); + +align_of :: ($T: Type) -> uint #expand { + return #run -> uint { + info := type_info(struct{ p: u8; t: T; }); + return info.members[1].offset_in_bytes.(uint); + }; +} + +bitcast :: ($T: Type, expr: Code) -> T #expand { + value := expr; + return (*value).(*T).*; +} + +power_of_two :: (x: uint) -> bool { + if x == 0 return false; + return x & (x - 1) == 0; +} + +align_forward :: (ptr: uint, align: uint = Default_Align) -> uint { + basic.assert(power_of_two(align), "alignment must be a power of two"); + + p := ptr; + mod := p & (align - 1); + if mod != 0 then p += align - mod; + return p; +} + diff --git a/module.jai b/module.jai new file mode 100644 index 0000000..6cee301 --- /dev/null +++ b/module.jai @@ -0,0 +1,26 @@ +uint :: #type u64; +rune :: #type u32; +rawptr :: #type *void; + +#load "utils.jai"; +#load "memory.jai"; +#load "macros.jai"; +#load "array.jai"; + + +#scope_module; + +basic :: #import "Basic"; +compiler :: #import "Compiler"; + +RUN_TESTS :: true; + + +#scope_file; + +#if RUN_TESTS { + #run compiler.set_build_options_dc(.{ do_output = false }); + + #import,file "./encoding/module.jai"(RUN_TESTS); +} + diff --git a/test/module.jai b/test/module.jai new file mode 100644 index 0000000..e784331 --- /dev/null +++ b/test/module.jai @@ -0,0 +1,48 @@ +T :: struct { + location: Source_Code_Location; + expects_run: int; + expects_ok: int; + failed: bool; +} + +Proc :: #type (t: *T); + +expect :: (t: *T, cond: bool, message := "", args: ..Any, loc := #caller_location) { + t.expects_run += 1; + if cond { + t.expects_ok += 1; + return; + } + + msg := "expectation failed"; + if message.count != 0 { + msg = basic.tprint(message, ..args); + } + + t.failed = true; + if #compile_time { + compiler.compiler_report(msg, loc = loc, mode = .ERROR_CONTINUABLE); + } + else { + basic.assert(false, msg, loc = loc); + } +} + +run :: (name: string, proc: Proc, loc := #caller_location) { + filename := strings.path_filename(loc.fully_pathed_filename); + basic.print("test %,%: %...", filename, loc.line_number, name); + + t: T; + proc(*t); + + if t.failed { + basic.print(" failed!\n"); + } + else { + basic.print(" ok!\n"); + } +} + +basic :: #import "Basic"; +strings :: #import "String"; +compiler :: #import "Compiler"; diff --git a/utils.jai b/utils.jai new file mode 100644 index 0000000..e8d9f84 --- /dev/null +++ b/utils.jai @@ -0,0 +1,42 @@ +rune_width :: (r: rune) -> int { + if r < 0 return -1; + + if r <= 0x7F return 1; + if r <= 0x7FF return 2; + + if r >= 0xD800 && + r <= 0xDFFF return -1; + + if r <= 0xFFFF return 3; + if r <= 0x10FFFF return 4; + + return -1; +} + +use_pascal_names :: (names: []string) { + for name: names { + b: basic.String_Builder; + + upper := false; + for i: 0..name.count - 1 { + c := name[i]; + if c == #char "_" { + upper = true; + continue; + } + + if i == 0 { + upper = true; + } + + if upper { + basic.append(*b, basic.to_upper(c)); + upper = false; + } else { + basic.append(*b, c); + } + } + + names[it_index] = basic.builder_to_string(*b); + } +}