This commit is contained in:
Judah Caruso 2025-05-11 02:29:36 -06:00
parent c216065ae8
commit c42131d90b
9 changed files with 622 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.build/

96
array.jai Normal file
View file

@ -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);
});
}

247
encoding/base64.jai Normal file
View file

@ -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);
});
}

4
encoding/module.jai Normal file
View file

@ -0,0 +1,4 @@
#module_parameters(RUN_TESTS := false);
#load "base64.jai";

126
macros.jai Normal file
View file

@ -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);
});
}

32
memory.jai Normal file
View file

@ -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;
}

26
module.jai Normal file
View file

@ -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);
}

48
test/module.jai Normal file
View file

@ -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";

42
utils.jai Normal file
View file

@ -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);
}
}