From: cassowarii Date: Thu, 25 Jun 2026 06:05:44 +0000 (-0700) Subject: reference counting + copy on write semantics + more string ops X-Git-Url: https://www.git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=bdb88958f07ba8b80b8df6031cab874d993b58bf;p=sarabande.git reference counting + copy on write semantics + more string ops --- diff --git a/src/data/string.c b/src/data/string.c index be9bd1c..f1f9d0e 100644 --- a/src/data/string.c +++ b/src/data/string.c @@ -1,13 +1,16 @@ #include "data/string.h" #include "mem/mem.h" +#include "gc/gcinfo.h" +#include "gc/rc.h" #include #define INLINE_BUFFER_SIZE 256 #define STRINGS_PER_BLOCK 256 -/* highest bit set on handle value means it is a tinystr */ -#define FLAG_TINY (1ULL << 63) +/* highest bit unset on handle value means it is a tinystr. this is + * so that 0x0000_0000_0000_0000 represents an empty string */ +#define FLAG_NONTINY (1ULL << 63) /* if string length < REQ_ALLOC_LENGTH, we can just put the * string in the handle itself. this should be 64 / 8 = 8, @@ -15,6 +18,13 @@ * uses one byte to store the string length as well. */ #define REQ_ALLOC_LENGTH (sizeof(hString)/sizeof(char)) +/* note: policy here is that, for consistency, all length values *don't* + * account for the null terminator, so all buffers' allocations should + * be one byte longer than their length and hold a NUL there. + * e.g. tinystr can hold max of 7 characters + a length, but the scratch + * buffers taken by certain functions should be 8 bytes, because we need + * to put a NUL at the end */ + /* TODO: Move these globals into some kind of "execution context" struct * that we can pass around to be reentrant */ /* also TODO a better (slightly more annoying to code) way to structure @@ -28,6 +38,7 @@ sbBuffer g_string_blocks; usize g_free_block_index; typedef struct strentry { + GCINFO gc; usize length; hString handle; flag is_external; @@ -46,12 +57,18 @@ typedef struct strblk { } strblk; static const char *get_ptr_of_entry(strentry *e); +static char *get_mutable_ptr_of_entry(strentry *e, usize length, hString *handle); +static strentry *find_entry_for_handle(hString handle); static strblk *get_strblk(usize index); static strentry *get_entry(strblk *blk, usize index); static strentry *new_entry(usize length); static void place_str_at_offset(strentry *e, usize offset, const char *str, usize length); static flag is_tinystr(hString handle); +static usize tinystr_length(hString handle); +static void tinystr_into_buffer(char *buffer, hString handle, usize max_length); +static hString tinystr_from_buffer(const char *buffer, usize length); static strblk *new_strblk(); +static void set_entry_size(strentry *e, usize length); void sbString_sys_init() { g_free_block_index = 0; @@ -72,61 +89,166 @@ hString sbString_new(const char *value, usize length) { if (length >= REQ_ALLOC_LENGTH) { strentry *e = new_entry(length); place_str_at_offset(e, 0, value, length); + RC_retain(e); return e->handle; } else { /* tinystr */ - hString new_handle = 0; - /* we encode the string backwards so it's easier to decode */ - for (int i = 0; i < length; i++) { - new_handle <<= 8; - new_handle |= value[length - i - 1]; - } - /* set length in high bit */ - new_handle |= (((u64)length << (8 * 7)) | FLAG_TINY); - return new_handle; + return tinystr_from_buffer(value, length); } } const char *sbString_get_value(hString handle, char *buffer_i_might_use, usize *length_out) { if (is_tinystr(handle)) { - /* string is stored backwards because it's easiest to just get the low byte */ - usize tinylength = ((handle >> (8 * 7)) & 0x7F); + usize tinylength = tinystr_length(handle); if (length_out) *length_out = tinylength; if (buffer_i_might_use) { - usize index = 0; - hString handle_to_eat = handle; - while (index < tinylength) { - buffer_i_might_use[index] = (handle_to_eat & 0xFF); /* get lowest byte and move down */ - handle_to_eat >>= 8; - index ++; - } - buffer_i_might_use[index] = '\0'; /* don't forget NUL! */ + tinystr_into_buffer(buffer_i_might_use, handle, tinylength); + /* pointer aliasing ! fun ! we love ! */ + return buffer_i_might_use; } - return buffer_i_might_use; } else { - strblk *blk = get_strblk(handle / STRINGS_PER_BLOCK); - strentry *entry = get_entry(blk, handle % STRINGS_PER_BLOCK); - if (length_out) *length_out = entry->length; - return get_ptr_of_entry(entry); + strentry *e = find_entry_for_handle(handle); + if (length_out) *length_out = e->length; + if (buffer_i_might_use) return get_ptr_of_entry(e); + } + return NULL; +} + +hString sbString_clone(hString handle) { + if (is_tinystr(handle)) { + /* tinystr's already have value semantics */ + return handle; + } else { + /* copying is lazy: we actually just additionally 'retain' + * the same entry multiple times. if we try to mutate an + * entry that's referenced in multiple places, then the + * system will lazily 'copy' the one that is being modified + * at that point. "COW semantics" */ + strentry *e = find_entry_for_handle(handle); + RC_retain(e); + return e->handle; + } +} + +void sbString_release(hString handle) { + if (is_tinystr(handle)) { + /* do nothing! */ + } else { + /* decrement refcount */ + strentry *e = find_entry_for_handle(handle); + RC_release(e); + } +} + +hString sbString_joined(hString a, hString b) { + usize a_length, b_length, final_length; + char a_scratch[8], b_scratch[8]; + const char *a_str, *b_str; + + a_str = sbString_get_value(a, a_scratch, &a_length); + b_str = sbString_get_value(b, b_scratch, &b_length); + final_length = a_length + b_length; + + if (final_length < REQ_ALLOC_LENGTH) { + /* tinystr */ + char scratch[16]; + memcpy(scratch, a_str, a_length); + memcpy(scratch + a_length, b_str, b_length); + return tinystr_from_buffer(scratch, final_length); + } else { + strentry *result; + if (!is_tinystr(a) && find_entry_for_handle(a)->gc.refcount == 0) { + result = find_entry_for_handle(a); + set_entry_size(result, final_length); + } else { + result = new_entry(final_length); + place_str_at_offset(result, 0, a_str, a_length); + } + + place_str_at_offset(result, a_length, b_str, b_length); + RC_retain(result); + + return result->handle; + } +} + +char *sbString_get_mutable_buffer(hString *handle, usize length_req, char *buffer_i_might_use, usize *length_out) { + if (!buffer_i_might_use) return NULL; + + flag use_orig_length = (length_req == 0 && length_out != NULL); + + if (is_tinystr(*handle)) { + usize length = use_orig_length ? tinystr_length(*handle) : length_req; + + if (length < REQ_ALLOC_LENGTH) { + /* lucky! we can just use the stack buffer! */ + tinystr_into_buffer(buffer_i_might_use, *handle, length); + if (length_out) *length_out = length; + return buffer_i_might_use; + } else { + /* upgrading a tinystr to a real strentry. need to replace the handle + * with one that refers to the new allocated version instead */ + strentry *e = new_entry(length); + char *buffer = get_mutable_ptr_of_entry(e, length, handle); + tinystr_into_buffer(buffer, *handle, length); + if (length_out) *length_out = length; + return buffer; + } + } else { + strentry *e = find_entry_for_handle(*handle); + usize length = use_orig_length ? e->length : length_req; + + if (length_out) *length_out = length; + return get_mutable_ptr_of_entry(e, length, handle); + } +} + +/* call after get_mutable_buffer to put the data back in the string if needed + * (it usually does nothing, but for tinystr it is needed if it remains tiny) */ +void sbString_fix_new_value(hString *handle, const char *new_value, usize length) { + if (length < REQ_ALLOC_LENGTH) { + /* now tinystr (regardless of what happened before). + * leave the old string for the garbage collector */ + *handle = tinystr_from_buffer(new_value, length); + } else { + strentry *existing_entry = find_entry_for_handle(*handle); /* null if tinystr */ + if (existing_entry) return; /* we already set it */ + + PANIC("inconsistent string lengths between sbString_get_mutable_buffer" + " and sbString_fix_new_value!"); } } usize sbString_get_length(hString handle) { - if (handle & FLAG_TINY) { + if (handle & FLAG_NONTINY) { + /* length is just in the entry */ + return find_entry_for_handle(handle)->length; + } else { /* length is top byte except high bit */ return (handle >> (8 * 7)) & 0x7F; - } else { - /* length is just in the entry */ - strblk *blk = get_strblk(handle / STRINGS_PER_BLOCK); - strentry *entry = get_entry(blk, handle % STRINGS_PER_BLOCK); - return entry->length; } } /* --- */ static flag is_tinystr(hString handle) { - return ((handle & FLAG_TINY) != 0); + return ((handle & FLAG_NONTINY) == 0); +} + +static usize tinystr_length(hString handle) { + return ((handle >> (8 * 7)) & 0x7F); +} + +static strentry *find_entry_for_handle(hString handle) { + if (is_tinystr(handle)) return NULL; + strblk *blk = get_strblk((handle & ~FLAG_NONTINY) / STRINGS_PER_BLOCK); + return get_entry(blk, (handle & ~FLAG_NONTINY) % STRINGS_PER_BLOCK); +} + +static strentry *duplicate_strentry(strentry *e, usize length) { + strentry *e_copy = new_entry(length); + place_str_at_offset(e_copy, 0, get_ptr_of_entry(e), length); + return e_copy; } static const char *get_ptr_of_entry(strentry *e) { @@ -137,14 +259,71 @@ static const char *get_ptr_of_entry(strentry *e) { } } -static char *get_mutable_ptr_of_entry(strentry *e) { - /* TODO: When we have static strings, panic here if we try to get - * a mutable ptr to one of them. (Should copy instead first.) */ - if (e->is_external) { - return e->somewhere_else_value.data; +static char *get_mutable_ptr_of_entry(strentry *e, usize length, hString *handle) { + /* TODO: When we have static strings, we should copy them to a new + * entry in order to get a mutable pointer to them. */ + if (e->gc.refcount < 2) { + /* 0 or 1 refs: can mutate string in place */ + set_entry_size(e, length); + + if (handle) *handle = e->handle; + if (e->is_external) { + return e->somewhere_else_value.data; + } else { + return e->inline_value; + } } else { - return e->inline_value; + /* 2+ refs: need to make a copy of the string to mutate so we don't + * affect the other versions */ + strentry *e_copy = duplicate_strentry(e, length); + + /* transfer one refcount to new string */ + RC_retain(e_copy); + RC_release(e); + + if (handle) *handle = e_copy->handle; + if (e->is_external) { + return e->somewhere_else_value.data; + } else { + return e->inline_value; + } + } +} + +static void tinystr_into_buffer(char *buffer, hString handle, usize max_length) { + /* we have max_length because we may need to cut off early in some corner cases, + * like getting a mutable ptr of length 3 for a tinystr of length 6 */ + if (!is_tinystr(handle)) PANIC("%llx is not a tinystr! can't parse!", handle); + + usize index = 0; + usize tinylength = tinystr_length(handle); + hString handle_to_eat = handle; + + if (tinylength >= REQ_ALLOC_LENGTH) PANIC("%llx has improper tinylength %zu", handle, tinylength); + + /* string is stored backwards because it's easiest to just get the low byte */ + while (index < tinylength && index < max_length) { + buffer[index] = (handle_to_eat & 0xFF); /* get lowest byte and move down */ + handle_to_eat >>= 8; + index ++; } + buffer[index] = '\0'; /* don't forget NUL! */ +} + +static hString tinystr_from_buffer(const char *buffer, usize length) { + if (length >= REQ_ALLOC_LENGTH) PANIC("cannot create tinystr of length %zu", length); + + hString new_handle = 0; + /* we encode the string backwards so it's easier to decode */ + for (int i = 0; i < length; i++) { + new_handle <<= 8; + new_handle |= buffer[length - i - 1]; + } + + /* set length in high byte */ + new_handle |= ((u64)length << (8 * 7)); + + return new_handle; } static strblk *get_strblk(usize index) { @@ -234,9 +413,9 @@ static void set_entry_size(strentry *e, usize length) { if (e->length > 0) { sbBuffer_append(&new_buf, e->inline_value, e->length + 1); } - e->length = length; e->is_external = TRUE; e->somewhere_else_value = new_buf; + e->length = length; e->somewhere_else_value.data[length] = '\0'; } else { /* we can remain inline, but need to set NUL in the right place */ @@ -252,14 +431,20 @@ static void place_str_at_offset(strentry *e, usize offset, const char *str, usiz usize max_bytes_to_write = e->length - offset; if (length < max_bytes_to_write) max_bytes_to_write = length; - char *where_to_put = get_mutable_ptr_of_entry(e) + offset; + char *where_to_put; + if (e->is_external) { + where_to_put = &e->somewhere_else_value.data[offset]; + } else { + where_to_put = &e->inline_value[offset]; + } + memcpy(where_to_put, str, max_bytes_to_write); } static strentry *new_entry(usize length) { strblk *new_block = find_free_block(); strentry *new_entry = alloc_entry(new_block); - new_entry->handle = new_block->id * STRINGS_PER_BLOCK + (new_entry - new_block->entries); + new_entry->handle = ((new_block->id * STRINGS_PER_BLOCK + (new_entry - new_block->entries)) | FLAG_NONTINY); set_entry_size(new_entry, length); return new_entry; } diff --git a/src/data/string.h b/src/data/string.h index 467dc5f..20098d2 100644 --- a/src/data/string.h +++ b/src/data/string.h @@ -1,16 +1,75 @@ #include "common.h" +#define OBJSL(value) (sbString_new(value, sizeof(value) - 1)) + typedef u64 hString; +/* create hString: + * + * hString hs = sbString_new(charptr, len); // length does not include NUL + * hString hs = sbString_new("Hello world!", 12); + * hString hs = OBJSL("Hello world!"); + * + * get hString length: + * + * usize length; + * sbString_get_value(hs, NULL, &length); + * + * hString -> c-string to read: + * + * char scratch[8]; // must be at least 8 + * const char *value = sbString_get_value(hs, scratch, NULL); + * + * of course, you can also get both the content and length at once by passing both parameters. + * + * hString -> c-string to edit: + * + * char scratch[8]; // must be at least 8 + * usize length_needed = ...; + * char *buffer_to_modify = sbString_get_mutable_buffer(hs, length_needed, scratch, NULL); + * // do something with buffer_to_modify + * sbString_fix_new_value(&hs, buffer_to_modify, length_needed); + * + * if you don't intend to change the string length, you can do it like: + * + * char scratch[8]; // must be at least 8 + * usize string_length; + * char *buffer_to_modify = sbString_get_mutable_buffer(hs, 0, scratch, &string_length); + * // do something with buffer_to_modify; string length is in string_length + * sbString_fix_new_value(&hs, buffer_to_modify, string_length); + * + * note that the (const) char * returned may refer to the 'scratch' buffer, so these + * pointers are not generally safe to return or save. just use hString for long-term string + * objects, only use c-strings when temporarily manipulating string data + * + * to make a copy of hString: + * + * hString copied_hs = sbString_clone(hs); + * + * to release a copy of hString: + * + * sbString_release(copied_hs); + * + * these copies are not actually copies, but rather are used to track a reference count to + * lazily copy strings when modification makes them necessary. the reference counter is not + * actually used for memory management, so failing to release an hString will not result in + * a memory leak, but the refcount is used to reduce the number of copies, so it pays to + * release clones that you're done with. furthermore, since objects aren't actually freed by + * their reference count hitting 0, you can reduce the refcount *before* doing something like + * a concatenation to reuse a string object that has 0 refs. + */ + hString sbString_new(const char *value, usize length); -/* buffer_i_might_use should be a pointer to a 8-character (at least) stack allocated buffer. - * if tinystr, it will use this to store the string data returned, and then return a pointer - * to the buffer. if not tinystr, it will not use the buffer and will return a pointer to - * somewhere else. */ const char *sbString_get_value(hString handle, char *buffer_i_might_use, usize *length_out); -void sbString_sys_init(); +char *sbString_get_mutable_buffer(hString *handle, usize length_req, char *buffer_i_might_use, usize *length_out); +void sbString_fix_new_value(hString *handle, const char *new_value, usize length); -void sbString_sys_deinit(); +hString sbString_clone(hString handle); +void sbString_release(hString handle); +hString sbString_joined(hString a, hString b); + +void sbString_sys_init(); +void sbString_sys_deinit(); diff --git a/src/gc/gcinfo.c b/src/gc/gcinfo.c new file mode 100644 index 0000000..5d5727b --- /dev/null +++ b/src/gc/gcinfo.c @@ -0,0 +1,2 @@ +#include "gc/gcinfo.h" + diff --git a/src/gc/gcinfo.h b/src/gc/gcinfo.h new file mode 100644 index 0000000..20a316b --- /dev/null +++ b/src/gc/gcinfo.h @@ -0,0 +1,18 @@ +#ifndef __SARABANDE_GCINFO_H__ +#define __SARABANDE_GCINFO_H__ + +#include "global.h" + +enum GCFLAG { + GC_FLAG_COLORA = 1ULL, + GC_FLAG_COLORB = 2ULL, + GC_FLAG_COLORC = GC_FLAG_COLORA | GC_FLAG_COLORB, + GC_FLAG_PINNED = 4ULL, +}; + +typedef struct GCINFO { + u32 gcflags; + u32 refcount; +} GCINFO; + +#endif diff --git a/src/gc/rc.h b/src/gc/rc.h new file mode 100644 index 0000000..f3d3e64 --- /dev/null +++ b/src/gc/rc.h @@ -0,0 +1,22 @@ +#ifndef __SARABANDE_GCRC_H +#define __SARABANDE_GCRC_H + +#include "gc/gcinfo.h" + +/* "Heapy" objects use reference counting to implement copy-on-write + * semantics. They do not use this info to free objects; this (will be) + * handled by a tracing garbage collector. So, it is okay for a + * reference count to drop to 0 temporarily. */ + +void RC_retain(void *obj) { + ((GCINFO*)obj)->refcount ++; +} + +void RC_release(void *obj) { +#ifdef DEBUG + if (((GCINFO*)obj)->refcount == 0) PANIC("reference count (of obj @ %p) cannot drop below 0!", obj); +#endif + ((GCINFO*)obj)->refcount --; +} + +#endif diff --git a/src/main.c b/src/main.c index 3663642..f3af33f 100644 --- a/src/main.c +++ b/src/main.c @@ -21,6 +21,18 @@ int main(int argc, char **argv) { sbLexToken t; char scratch[8]; + printf("ok here i go\n"); + hString hello_str = OBJSL("hello! "); + hString long_str = OBJSL(""); + for (int i = 0; i < 200; i++) { + printf("NEW LOOP\n"); + sbString_release(long_str); + long_str = sbString_joined(long_str, hello_str); + printf("END_OF_LOOP\n"); + printf("result: '%s'\n", sbString_get_value(long_str, scratch, NULL)); + } + printf("%s\n", sbString_get_value(long_str, scratch, NULL)); + do { t = sbLexer_next(&lx);