/* * the core catskill c-library for * catskill source files transpiled to c. * used during bootstrapping, while the full * catskill standard library is not yet available. * * copyright (c) 2025, mel g. * * spdx-license-identifier: mpl-2.0 */ // TODO: as mentioned in `boot/common.c`, the catskill transpiled // source code "core" library should be merged with the catboot // "common" library. #pragma once // common headers for both your average catskill program. // other libc headers can be included with the pragma // `| c-header "header.h"`. #include #include #include #include #include #include #include #include #include #include #include // types predefined for catskill programs. // the transpiler outputs these types directly when seen // in the source code. #define uint8 uint8_t #define uint16 uint16_t #define uint32 uint32_t #define uint64 uint64_t #define int8 int8_t #define int16 int16_t #define int32 int32_t #define int64 int64_t #define float32 float #define float64 double #define real float64 #define uint uint64 #define integer int64 #define ascii char #define byte char #define bool _Bool #define true 1 #define false 0 #define nil NULL _Noreturn void panic(const ascii* message, ...) { fprintf(stderr, "panic: "); va_list args; va_start(args, message); vfprintf(stderr, message, args); va_end(args); fprintf(stderr, "\n"); exit(EXIT_FAILURE); } void* allocate(size_t size) { void* ptr = malloc(size); if (!ptr) panic("failed to allocate %zu bytes. are we out of memory? :(", size); return ptr; } void* reallocate(void* ptr, size_t size) { void* new_ptr = realloc(ptr, size); if (!new_ptr) panic("failed to reallocate %zu bytes. are we out of memory? :(", size); if (size == 0) return nil; return new_ptr; } struct _Array { uint element_size; void* data; uint length; uint capacity; }; // a dynamic array type. #define Array(type) struct _Array #define ARRAY_DEFAULT_CAPACITY 16 void array_grow(struct _Array* array); void array_grow_until(struct _Array* array, uint min_capacity); // internal use, try the `array_new` macro. struct _Array _array_new(uint element_size) { struct _Array array = { .element_size = element_size, .data = nil, .length = 0, .capacity = 0, }; array_grow(&array); return array; } // creates a new, empty array for the given type. #define array_new(type) _array_new(sizeof(type)) // returns the number of elements in the array. uint array_length(const struct _Array* array) { return array->length; } // returns the total number of elements the array can hold before reallocating. uint array_capacity(const struct _Array* array) { return array->capacity; } // true if the array is empty. bool array_is_empty(const struct _Array* array) { return array->length == 0; } // resizes the array backing allocation to the new capacity. // if the new capacity is smaller than the current length, // the array will be truncated to the new length. // if the new capacity is 0, the array's data will be freed. void array_resize_allocation(struct _Array* array, uint new_capacity) { // reallocate can handle data being nil, so we don't need to check for that. array->data = reallocate(array->data, new_capacity * array->element_size); array->capacity = new_capacity; array->length = array->length < new_capacity ? array->length : new_capacity; } // resizes the array to a new length. // if the new length is larger than the current capacity, // the array will be grown to fit, and the new elements will be zero-initialized. // if the new length is smaller than the current length, // the array will be truncated, and the discarded elements are zeroed out. // this function does not shrink the underlying allocation. void array_resize(struct _Array* array, uint new_length) { if (new_length > array->capacity) { // grow array to fit the new length naturally. array_grow_until(array, new_length); // initialize the new elements to zero. memset((uint8*)array->data + (array->length * array->element_size), 0, (new_length - array->length) * array->element_size); } else if (new_length < array->length) { // truncate the array elements to the new length. memset((uint8*)array->data + (new_length * array->element_size), 0, (array->length - new_length) * array->element_size); } array->length = new_length; } // frees the memory used by the array's data. void array_free(struct _Array* array) { array_resize_allocation(array, 0); } // grows the array's capacity naturally. void array_grow(struct _Array* array) { if (array->capacity == 0) array_resize_allocation(array, ARRAY_DEFAULT_CAPACITY); else array_resize_allocation(array, array->capacity * 2); } // grows the array's capacity naturally to hold at least `min_capacity` elements. // if current capacity is already enough, nothing happens. void array_grow_until(struct _Array* array, uint min_capacity) { while (array->capacity < min_capacity) array_grow(array); } // internal use, try the `array_get` macro. void _array_get(const struct _Array* array, uint index, void* out_element) { if (index >= array->length) panic("index out of bounds: %u (length: %u)", index, array->length); memcpy(out_element, (uint8*)array->data + (index * array->element_size), array->element_size); } // gets the element at a given index and copies it to `out_element`. // panic if the index is out of bounds. #define array_get(array, index, out_element) _array_get(array, index, (void*)(out_element)) // internal use, prefer the `array_set` macro. void _array_set(struct _Array* array, uint index, const void* element) { if (index >= array->length) { panic("index out of bounds: %u (length: %u)", index, array->length); } memcpy((uint8*)array->data + (index * array->element_size), element, array->element_size); } // sets the element at a given index. // panic if the index is out of bounds. #define array_set(array, index, element) _array_set(array, index, (const void*)(element)) // internal use, prefer the `array_at` macro. void* _array_at(const struct _Array* array, uint index) { if (index >= array->length) { panic("index out of bounds: %u (length: %u)", index, array->length); } return (uint8*)array->data + (index * array->element_size); } // returns a pointer to the element at a given index. // panics if the index is out of bounds. #define array_at(type, array, index) ((type*)_array_at(array, index)) // internal use, prefer the `array_insert` macro. void _array_insert(struct _Array* array, uint index, const void* element) { if (index > array->length) panic("index out of bounds: %u (length: %u)", index, array->length); array_grow_until(array, array->length + 1); // shift elements to the right to make space for the new element // if we are inserting at the end, we don't need to shift anything. // if there aren't any elements, we also don't need to shift anything. if (index < array->length && array->length > 0) memmove( (uint8*)array->data + ((index + 1) * array->element_size), (uint8*)array->data + (index * array->element_size), (array->length - index) * array->element_size); // insert the new element at the specified index memcpy((uint8*)array->data + (index * array->element_size), element, array->element_size); ++array->length; } // inserts an element at a given index, shifting subsequent elements. // panics if the index is out of bounds (greater than length). #define array_insert(array, index, element) _array_insert(array, index, (const void*)(element)) // adds an element to the end of the array. // internal use; prefer the `array_push` macro. void _array_push(struct _Array* array, const void* element) { array_insert(array, array->length, element); } // adds an element to the end of the array. #define array_push(array, element) _array_push(array, (const void*)(element)) // removes an element at a given index, shifting subsequent elements. // if `removed_element` is not null, the removed element is copied to it. // panics if the index is out of bounds. // internal use; prefer the `array_remove` macro. void _array_remove(struct _Array* array, uint index, void* removed_element) { if (index >= array->length) { panic("index out of bounds: %u (length: %u)", index, array->length); } if (removed_element) memcpy(removed_element, (uint8*)array->data + (index * array->element_size), array->element_size); // shift elements to the left to fill the gap // if we are removing the last element, we don't need to shift anything. // if there aren't any elements, there is also nothing to shift. if (index < array->length - 1 && array->length > 1) memmove( (uint8*)array->data + (index * array->element_size), (uint8*)array->data + ((index + 1) * array->element_size), (array->length - index - 1) * array->element_size); --array->length; } // removes an element at a given index, shifting subsequent elements. // if `removed_element` is not null, the removed element is copied to it. // panics if the index is out of bounds. #define array_remove(array, index, removed_element) \ _array_remove(array, index, (void*)(removed_element)) // removes and optionally returns the last element of the array. // panics if the array is empty. // internal use; prefer the `array_pop` macro. void _array_pop(struct _Array* array, void* removed_element) { if (array->length == 0) panic("cannot pop from an empty array"); array_remove(array, array->length - 1, removed_element); } // removes and optionally returns the last element of the array. // panics if the array is empty. #define array_pop(array, removed_element) _array_pop(array, (void*)(removed_element)) // clears the array, setting its length to 0. // this does not free the allocated memory. void array_clear(struct _Array* array) { memset(array->data, 0, array->element_size * array->length); array->length = 0; } // appends all elements from array `b` to the end of array `a`. // panics if the element sizes of the arrays are different. void array_append(struct _Array* a, struct _Array* b) { if (a->element_size != b->element_size) panic("cannot append arrays of different type and element size: %u vs %u", a->element_size, b->element_size); // grow array `a` to fit the new elements, following the array's growth strategy. array_grow_until(a, a->length + b->length); memcpy((uint8*)a->data + (a->length * a->element_size), b->data, b->length * a->element_size); a->length += b->length; } // a macro for iterating over each element in an array. #define FOR_EACH_ARRAY(type, array, action) \ for (uint i = 0; i < array_length(array); ++i) { \ type* element = array_at(type, array, i); \ action; \ } #define ARRAY_FREE_ELEMENTS(type, array, free_action) \ FOR_EACH_ARRAY(type, array, { free_action(element); }) // a macro for iterating over a range of elements in an array. #define FOR_EACH_IN_RANGE(type, array, start, end, action) \ for (uint i = start; i < end && i < array_length(array); ++i) { \ type* element = array_at(type, array, i); \ action; \ } // a dynamic string type. // the string is always null-terminated to be compatible with c functions. // the length of the string does not include the null terminator. struct String { Array(ascii) data; }; void string_ensure_capacity(struct String* str, uint new_len); // creates a new, empty string. struct String string_new(void) { struct String str; str.data = array_new(ascii); ((ascii*)str.data.data)[0] = '\0'; return str; } // creates a new string from a c-style string. struct String string_from(const ascii* c_str) { struct String str = string_new(); uint len = strlen(c_str); string_ensure_capacity(&str, len); memcpy(str.data.data, c_str, len); str.data.length = len; ((ascii*)str.data.data)[len] = '\0'; return str; } // frees the memory used by the string. void string_free(struct String* str) { array_free(&str->data); } // returns the length of the string. uint string_length(const struct String* str) { return str->data.length; } // returns the current capacity of the string (excluding the null terminator). uint string_capacity(const struct String* str) { return str->data.capacity > 0 ? str->data.capacity - 1 : 0; } Array(ascii) string_backing(const struct String* str) { return str->data; } // checks if the string is empty. bool string_is_empty(const struct String* str) { return str->data.length == 0; } // returns a null-terminated c-string representation. const ascii* string_c_str(const struct String* str) { return (const ascii*)str->data.data; } // ensures the array has enough capacity for `new_len` characters plus a null terminator. void string_ensure_capacity(struct String* str, uint new_len) { uint required_capacity = new_len + 1; if (required_capacity > str->data.capacity) { uint new_cap = str->data.capacity; if (new_cap == 0) new_cap = ARRAY_DEFAULT_CAPACITY; while (required_capacity > new_cap) new_cap *= 2; array_resize_allocation(&str->data, new_cap); } } // appends a character to the end of the string. void string_push(struct String* str, ascii c) { uint new_len = str->data.length + 1; string_ensure_capacity(str, new_len); ((ascii*)str->data.data)[str->data.length] = c; str->data.length = new_len; ((ascii*)str->data.data)[new_len] = '\0'; } // removes and returns the last character of the string. // panics if the string is empty. ascii string_pop(struct String* str) { if (str->data.length == 0) { panic("cannot pop from an empty string"); } str->data.length--; ascii c = ((ascii*)str->data.data)[str->data.length]; ((ascii*)str->data.data)[str->data.length] = '\0'; return c; } // appends a string to the end of another string. void string_append(struct String* dest, const struct String* src) { uint old_len = dest->data.length; uint new_len = old_len + src->data.length; string_ensure_capacity(dest, new_len); memcpy((ascii*)dest->data.data + old_len, src->data.data, src->data.length); dest->data.length = new_len; ((ascii*)dest->data.data)[new_len] = '\0'; } // appends a c-style string to the end of a string. void string_append_c_str(struct String* dest, const ascii* c_str) { uint c_str_len = strlen(c_str); uint old_len = dest->data.length; uint new_len = old_len + c_str_len; string_ensure_capacity(dest, new_len); memcpy((ascii*)dest->data.data + old_len, c_str, c_str_len); dest->data.length = new_len; ((ascii*)dest->data.data)[new_len] = '\0'; } // clears the string, making it empty. void string_clear(struct String* str) { str->data.length = 0; ((ascii*)str->data.data)[0] = '\0'; } // creates a copy of a string. struct String string_clone(const struct String* str) { struct String new_str = string_new(); string_ensure_capacity(&new_str, str->data.length); memcpy(new_str.data.data, str->data.data, str->data.length + 1); // copy with null terminator new_str.data.length = str->data.length; return new_str; } // compares two strings for equality. bool string_equals(const struct String* a, const struct String* b) { if (a->data.length != b->data.length) return false; if (a->data.length == 0) return true; return memcmp(a->data.data, b->data.data, a->data.length) == 0; } // compares a string with a c-style string for equality. bool string_equals_c_str(const struct String* a, const ascii* b) { uint b_len = strlen(b); if (a->data.length != b_len) return false; if (a->data.length == 0) return true; return memcmp(a->data.data, b, a->data.length) == 0; } // returns a new string that is a slice of the original. // the slice is from `start` (inclusive) to `end` (exclusive). // panics if the range is invalid. struct String string_slice(const struct String* str, uint start, uint end) { if (start > end || end > str->data.length) { panic("invalid slice range [%u, %u) for string of length %u", start, end, str->data.length); } uint slice_len = end - start; struct String new_str = string_new(); string_ensure_capacity(&new_str, slice_len); memcpy(new_str.data.data, (const ascii*)str->data.data + start, slice_len); new_str.data.length = slice_len; ((ascii*)new_str.data.data)[slice_len] = '\0'; return new_str; } // returns the character at the given index. // panics if the index is out of bounds. ascii string_get(const struct String* str, uint index) { if (index >= str->data.length) { panic("index out of bounds: %u (length: %u)", index, str->data.length); } return ((ascii*)str->data.data)[index]; } // sets the character at the given index. // panics if the index is out of bounds. void string_set(struct String* str, uint index, ascii c) { if (index >= str->data.length) { panic("index out of bounds: %u (length: %u)", index, str->data.length); } ((ascii*)str->data.data)[index] = c; } #define FOR_EACH_IN_STRING(character_variable, str, action) \ for (uint i = 0; i < string_length(str); ++i) { \ ascii character_variable = ((ascii*)((str)->data.data))[i]; \ action; \ } // a slice is a view into a block of memory. // it does not own the data it points to. struct _Slice { uint element_size; void* data; uint length; }; #define Slice(type) struct _Slice // internal use, try the `slice_new` macro. struct _Slice slice_new(void* data, uint length, uint element_size) { struct _Slice slice = { .data = data, .length = length, .element_size = element_size }; return slice; } #define slice_new(data, length) slice_new((void*)(data), (length), sizeof(*(data))) // a string_view is a slice of characters. // it is not null-terminated. struct String_View { Slice(ascii) data; }; uint string_view_length(const struct String_View view) { return view.data.length; } Slice(ascii) string_view_backing(const struct String_View view) { return view.data; } // creates a new string_view from a string. struct String_View string_view_from_string(const struct String* str) { return (struct String_View){ .data = slice_new(str->data.data, str->data.length) }; } // creates a new string_view from a c-style string. struct String_View string_view_from_c_str(const ascii* c_str) { return (struct String_View){ .data = slice_new((void*)c_str, strlen(c_str)) }; } struct String_View string_view_from_slice(Slice(ascii) slice) { return (struct String_View){ .data = slice }; } // compares two string_views for equality. bool string_view_equals(struct String_View a, struct String_View b) { Slice(ascii) a_backing = string_view_backing(a), b_backing = string_view_backing(b); if (string_view_length(a) != string_view_length(b)) return false; if (a.data.length == 0) return true; return memcmp(a_backing.data, b_backing.data, a_backing.length) == 0; } // a file handle. struct File { FILE* handle; struct String* path; }; // opens a file and returns a handle to it. // returns nil if the file cannot be opened. struct File* file_open(struct String* path, const ascii* mode) { FILE* handle = fopen(string_c_str(path), mode); if (!handle) return nil; struct File* file = allocate(sizeof(struct File)); file->handle = handle; file->path = path; return file; } // closes a file. void file_close(struct File* file) { if (!file) return; fclose(file->handle); free(file); } // reads the entire content of a file into a string. struct String file_read_all(struct File* file) { fseek(file->handle, 0, SEEK_END); long file_size = ftell(file->handle); fseek(file->handle, 0, SEEK_SET); struct String content = string_new(); string_ensure_capacity(&content, file_size); fread(content.data.data, 1, file_size, file->handle); content.data.length = file_size; ((ascii*)content.data.data)[file_size] = '\0'; return content; } // writes the content of a string to a file. void file_write(struct File* file, struct String* content) { fwrite(string_c_str(content), 1, string_length(content), file->handle); } // checks if a file exists at the given path. bool file_exists(struct String* path) { return access(string_c_str(path), F_OK) != -1; } // memory-maps a file and returns a string_view of its content. // panic if the file cannot be mapped. struct String_View file_map_to_string_view(struct File* file) { struct stat st; if (fstat(fileno(file->handle), &st) == -1) { panic("could not get file status for mmap"); } void* mapped = mmap(0, st.st_size, PROT_READ, MAP_PRIVATE, fileno(file->handle), 0); if (mapped == MAP_FAILED) { panic("could not map file to memory"); } return string_view_from_slice(slice_new(mapped, st.st_size)); } // unmaps a memory-mapped view. void file_unmap(struct File* file, struct String_View view) { (void)file; munmap(string_view_backing(view).data, string_view_length(view)); }