diff --git a/Makefile b/Makefile index 06087c6..31ebd24 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,10 @@ RFLAGS=-O3 MODE=release ifeq ($(MODE), release) CFLAGS=$(GFLAGS) $(RFLAGS) -else +else ifeq ($(MODE), debug) CFLAGS=$(GFLAGS) $(DFLAGS) +else ifeq ($(MODE), full) +CFLAGS=$(GFLAGS) $(DFLAGS) -DTEST_VERBOSE=1 endif # Units to compile diff --git a/alisp.org b/alisp.org index 21278fd..6173c07 100644 --- a/alisp.org +++ b/alisp.org @@ -45,11 +45,61 @@ i.e. no parsing. *** DONE Design what a "parser function" would look like The general function is something like ~stream -> T | Err~. What other state do we need to encode? -*** WIP Write a parser for integers +*** TODO Write a parser for integers *** TODO Write a parser for symbols *** TODO Write a parser for lists *** TODO Write a parser for vectors *** TODO Write the general parser +** WIP Unit tests :tests: +*** TODO Test streams +**** DONE Test file init +[[file:test/test_stream.c::void stream_test_file(void)]] +***** DONE Test successful init from real files +Ensure stream_size is 0 i.e. we don't read anything on creation. +Also ensure stream_eoc is false. +***** DONE Test failed init from fake files +**** DONE Test peeking and next +[[file:test/test_stream.c::void stream_test_peek_next(void)]] +- Peeking with bad streams ('\0' return) +- Peeking with good streams (no effect on position) +- Next with bad streams ('\0' return, no effect on position) +- Next with good streams (effects position) +- Peeking after next (should just work) +**** DONE Test seeking +[[file:test/test_stream.c::void stream_test_seek(void)]] +- Seeking forward/backward on a bad stream (should stop at 0) +- Seeking forward/backward too far (should clamp) +- Seeking forward/backward zero sum via relative index (stream_seek) +**** DONE Test substring +[[file:test/test_stream.c::void stream_test_substr(void)]] +- Substr on bad stream (NULL sv) +- Substr on bad position/size (NULL sv) +- Substr relative/absolute (good SV) +**** TODO Test till +[[file:test/test_stream.c::void stream_test_till(void)]] +- till on a bad stream (NULL SV) +- till on an ended stream (NULL SV) +- till on a stream with no items in search string (eoc) +- till on a stream with all items in search string (no effect) +- till on a stream with prefix being all search string (no effect) +- till on a stream with suffix being all search string (stops at + suffix) +**** TODO Test while +[[file:test/test_stream.c::void stream_test_while(void)]] +- while on a bad stream (NULL SV) +- while on an ended stream (NULL SV) +- while on a stream with no items in search string (no effect) +- while on a stream with all items in search string (eoc) +- while on a stream with prefix being all search string (effect) +- while on a stream with suffix being all search string (no effect) +**** TODO Test line_col +[[file:test/test_stream.c::void stream_test_line_col(void)]] +- line_col on bad stream (no effect on args) +- line_col on eoc stream (should go right to the end) +- line_col on random points in a stream +*** DONE Test system registration of allocated units +In particular, does clean up work as we expect? Do we have situations +where we may double free or not clean up something we should've? ** Backlog *** TODO Design Big Integers We currently have 62 bit integers implemented via immediate values @@ -139,9 +189,6 @@ Latter approach time complexity: Former approach is better time complexity wise, but latter is way better in terms of simplicity of code. Must deliberate. -*** TODO Test system registration of allocated units :test: -In particular, does clean up work as we expect? Do we have situations -where we may double free or not clean up something we should've? *** TODO Design Strings We have ~sv_t~ so our basic C API is done. We just need pluggable functions to construct and deconstruct strings as lisps. @@ -149,6 +196,12 @@ functions to construct and deconstruct strings as lisps. Should we capitalise symbols? This way, we limit the symbol table's possible options a bit (potentially we could design a better hashing algorithm?) and it would be kinda like an actual Lisp. +*** TODO sv_t +[[file:include/alisp/sv.h::/// String Views]] +**** TODO sv_substr +Takes an index and a size, returns a string view to that substring. +**** TODO sv_chop_left and sv_chop_right +Super obvious. ** Completed *** DONE Test value constructors and destructors :test: Test if ~make_int~ works with ~as_int,~ ~intern~ with ~as_sym~. @@ -160,4 +213,4 @@ Test if ~make_vec~ works with ~as_vec~, ~cons~ with ~as_cons~ AND We may need to think of effective ways to deal with NILs in ~car~ and ~cdr~. Maybe make functions as well as the macros so I can choose between them? -**** DONE Write more tests +*** DONE Write more tests diff --git a/include/alisp/lisp.h b/include/alisp/lisp.h index ce52176..94f2caf 100644 --- a/include/alisp/lisp.h +++ b/include/alisp/lisp.h @@ -49,6 +49,8 @@ vec_t *as_vec(lisp_t *); lisp_t *car(lisp_t *); lisp_t *cdr(lisp_t *); +void lisp_free(lisp_t *); + #endif /* Copyright (C) 2026 Aryadev Chavali diff --git a/include/alisp/stream.h b/include/alisp/stream.h index 144c771..36fa09c 100644 --- a/include/alisp/stream.h +++ b/include/alisp/stream.h @@ -40,7 +40,7 @@ typedef struct typedef struct { stream_type_t type; - char *name; + const char *name; u64 position; union { @@ -51,9 +51,9 @@ typedef struct #define STREAM_DEFAULT_CHUNK 64 -stream_err_t stream_init_string(stream_t *, char *, sv_t); -stream_err_t stream_init_pipe(stream_t *, char *, FILE *); -stream_err_t stream_init_file(stream_t *, char *, FILE *); +stream_err_t stream_init_string(stream_t *, const char *, sv_t); +stream_err_t stream_init_pipe(stream_t *, const char *, FILE *); +stream_err_t stream_init_file(stream_t *, const char *, FILE *); void stream_stop(stream_t *); // End of Content (i.e. we've consumed all cached content/file) @@ -66,9 +66,9 @@ char stream_next(stream_t *); // Peek current character, do not push position char stream_peek(stream_t *); // Move forward or backward in the stream, return success of operation -bool stream_seek(stream_t *, i64); -bool stream_seek_forward(stream_t *, u64); -bool stream_seek_backward(stream_t *, u64); +u64 stream_seek(stream_t *, i64); +u64 stream_seek_forward(stream_t *, u64); +u64 stream_seek_backward(stream_t *, u64); // Return a relative substring of a given size sv_t stream_substr(stream_t *, u64); diff --git a/include/alisp/symtable.h b/include/alisp/symtable.h index 21f3de5..adbff50 100644 --- a/include/alisp/symtable.h +++ b/include/alisp/symtable.h @@ -22,7 +22,7 @@ typedef struct void sym_table_init(sym_table_t *); char *sym_table_find(sym_table_t *, sv_t); -void sym_table_cleanup(sym_table_t *); +void sym_table_free(sym_table_t *); #endif diff --git a/src/lisp.c b/src/lisp.c index 77dcf24..a180928 100644 --- a/src/lisp.c +++ b/src/lisp.c @@ -26,7 +26,7 @@ void sys_free(sys_t *sys) { static_assert(NUM_TAGS == 5); - sym_table_cleanup(&sys->symtable); + sym_table_free(&sys->symtable); if (sys->memory.size == 0) return; @@ -34,26 +34,7 @@ void sys_free(sys_t *sys) for (size_t i = 0; i < VEC_SIZE(&sys->memory, lisp_t **); ++i) { lisp_t *allocated = VEC_GET(&sys->memory, i, lisp_t *); - switch (get_tag(allocated)) - { - case TAG_CONS: - // Delete the cons - free(as_cons(allocated)); - break; - case TAG_VEC: - { - vec_t *vec = as_vec(allocated); - vec_free(vec); - free(vec); - break; - } - case TAG_NIL: - case TAG_INT: - case TAG_SYM: - case NUM_TAGS: - // shouldn't be dealt with (either constant or dealt with elsewhere) - break; - } + lisp_free(allocated); } // Free the container @@ -110,6 +91,30 @@ lisp_t *cdr(lisp_t *lsp) return CDR(lsp); } +void lisp_free(lisp_t *item) +{ + switch (get_tag(item)) + { + case TAG_CONS: + // Delete the cons + free(as_cons(item)); + break; + case TAG_VEC: + { + vec_t *vec = as_vec(item); + vec_free(vec); + free(vec); + break; + } + case TAG_NIL: + case TAG_INT: + case TAG_SYM: + case NUM_TAGS: + // shouldn't be dealt with (either constant or dealt with elsewhere) + break; + } +} + /* Copyright (C) 2025, 2026 Aryadev Chavali * This program is distributed in the hope that it will be useful, but WITHOUT diff --git a/src/main.c b/src/main.c index 681ecef..2529e4b 100644 --- a/src/main.c +++ b/src/main.c @@ -23,7 +23,10 @@ void usage(FILE *fp) int main(int argc, char *argv[]) { - int ret = 0; + int ret = 0; + FILE *pipe = NULL; + stream_t stream = {0}; + if (argc == 1) { usage(stderr); @@ -35,8 +38,6 @@ int main(int argc, char *argv[]) TODO("alisp doesn't support multiple files currently."); } - FILE *fp = NULL; - stream_t stream = {0}; if (strncmp(argv[1], "--", 2) == 0) { stream_err_t err = stream_init_pipe(&stream, "stdin", stdin); @@ -55,8 +56,8 @@ int main(int argc, char *argv[]) } else { - fp = fopen(argv[1], "rb"); - stream_err_t err = stream_init_file(&stream, argv[1], fp); + pipe = fopen(argv[1], "rb"); + stream_err_t err = stream_init_file(&stream, argv[1], pipe); if (err) { fprintf(stderr, "ERROR: %s from `%s`\n", stream_err_to_cstr(err), @@ -68,9 +69,9 @@ int main(int argc, char *argv[]) LOG("[INFO]: Initialised stream for `%s`\n", stream.name); end: - if (fp) - fclose(fp); stream_stop(&stream); + if (pipe) + fclose(pipe); return ret; } diff --git a/src/stream.c b/src/stream.c index e03c5ef..b332275 100644 --- a/src/stream.c +++ b/src/stream.c @@ -5,6 +5,7 @@ * Commentary: */ +#include #include #include @@ -35,7 +36,8 @@ const char *stream_err_to_cstr(stream_err_t err) } } -stream_err_t stream_init_string(stream_t *stream, char *name, sv_t contents) +stream_err_t stream_init_string(stream_t *stream, const char *name, + sv_t contents) { if (!stream) return STREAM_ERR_INVALID_PTR; @@ -49,7 +51,7 @@ stream_err_t stream_init_string(stream_t *stream, char *name, sv_t contents) return STREAM_ERR_OK; } -stream_err_t stream_init_pipe(stream_t *stream, char *name, FILE *pipe) +stream_err_t stream_init_pipe(stream_t *stream, const char *name, FILE *pipe) { if (!stream) return STREAM_ERR_INVALID_PTR; @@ -68,7 +70,7 @@ stream_err_t stream_init_pipe(stream_t *stream, char *name, FILE *pipe) return STREAM_ERR_OK; } -stream_err_t stream_init_file(stream_t *stream, char *name, FILE *pipe) +stream_err_t stream_init_file(stream_t *stream, const char *name, FILE *pipe) { if (!stream) return STREAM_ERR_INVALID_PTR; @@ -82,8 +84,6 @@ stream_err_t stream_init_file(stream_t *stream, char *name, FILE *pipe) stream->name = name; stream->pipe.file = pipe; - vec_init(&stream->pipe.cache, STREAM_DEFAULT_CHUNK); - return STREAM_ERR_OK; } @@ -96,8 +96,11 @@ void stream_stop(stream_t *stream) case STREAM_TYPE_STRING: free(stream->string.data); break; - case STREAM_TYPE_PIPE: case STREAM_TYPE_FILE: + // ensure we reset the FILE pointer to the start + fseek(stream->pipe.file, 0, SEEK_SET); + // fallthrough + case STREAM_TYPE_PIPE: // Must cleanup vector vec_free(&stream->pipe.cache); break; @@ -236,7 +239,7 @@ char stream_peek(stream_t *stream) } } -bool stream_seek(stream_t *stream, i64 offset) +u64 stream_seek(stream_t *stream, i64 offset) { if (offset < 0) return stream_seek_backward(stream, offset * -1); @@ -247,20 +250,20 @@ bool stream_seek(stream_t *stream, i64 offset) return true; } -bool stream_seek_forward(stream_t *stream, u64 offset) +u64 stream_seek_forward(stream_t *stream, u64 offset) { if (stream_eoc(stream)) - return false; + return 0; switch (stream->type) { case STREAM_TYPE_STRING: { if (stream->position + offset >= stream->string.size) - return false; + return 0; stream->position += offset; - return true; + return offset; } case STREAM_TYPE_PIPE: case STREAM_TYPE_FILE: @@ -271,7 +274,7 @@ bool stream_seek_forward(stream_t *stream, u64 offset) if (stream->position + offset < stream->pipe.cache.size) { stream->position += offset; - return true; + return offset; } // Try to read chunks in till we've reached it or we're at the end of the @@ -283,9 +286,11 @@ bool stream_seek_forward(stream_t *stream, u64 offset) // Same principle as the stream_eoc(stream) check. if (stream->position + offset > stream->pipe.cache.size) - return false; + { + offset = stream->pipe.cache.size - stream->position; + } stream->position += offset; - return true; + return offset; } default: FAIL("Unreachable"); @@ -293,13 +298,16 @@ bool stream_seek_forward(stream_t *stream, u64 offset) } } -bool stream_seek_backward(stream_t *stream, u64 offset) +u64 stream_seek_backward(stream_t *stream, u64 offset) { assert(stream); if (stream->position < offset) - return false; + { + offset = stream->position; + } + stream->position -= offset; - return true; + return offset; } sv_t stream_substr(stream_t *stream, u64 size) @@ -309,11 +317,11 @@ sv_t stream_substr(stream_t *stream, u64 size) // See if I can go forward enough to make this substring u64 current_position = stream->position; - bool successful = stream_seek_forward(stream, size); + u64 successful = stream_seek_forward(stream, size); // Reset the position in either situation stream->position = current_position; - if (!successful) + if (successful != size) return SV(NULL, 0); char *ptr = NULL; diff --git a/src/sv.c b/src/sv.c index d6fcf0a..1fb8521 100644 --- a/src/sv.c +++ b/src/sv.c @@ -12,6 +12,10 @@ sv_t sv_copy(sv_t old) { + if (old.size == 0) + return SV(old.data, 0); + else if (old.data == NULL) + return SV(NULL, old.size); char *newstr = calloc(1, (old.size + 1) * sizeof(*newstr)); memcpy(newstr, old.data, old.size); newstr[old.size] = '\0'; diff --git a/src/symtable.c b/src/symtable.c index 5813177..50c8e3a 100644 --- a/src/symtable.c +++ b/src/symtable.c @@ -54,7 +54,7 @@ char *sym_table_find(sym_table_t *table, sv_t sv) return ENTRY_GET(table, index).data; } -void sym_table_cleanup(sym_table_t *table) +void sym_table_free(sym_table_t *table) { // Iterate through the strings and free each of them. sv_t current = {0}; diff --git a/test/main.c b/test/main.c index 29071ac..d684f93 100644 --- a/test/main.c +++ b/test/main.c @@ -6,28 +6,33 @@ */ #include +#include #include +#include #include "./data.h" #include "./test.h" #include "./test_lisp_api.c" +#include "./test_stream.c" +#include "./test_sv.c" +#include "./test_symtable.c" #include "./test_vec.c" test_suite_t SUITES[] = { - LISP_API_SUITE, - VEC_SUITE, + SV_SUITE, VEC_SUITE, SYMTABLE_SUITE, STREAM_SUITE, LISP_API_SUITE, }; int main(void) { + // Seed the pseudorandom gen for subsequent tests. + srand(time(NULL)); for (u64 i = 0; i < ARRSIZE(SUITES); ++i) { test_suite_t suite = SUITES[i]; printf("Suite [%s]\n", suite.name); for (u64 j = 0; j < suite.size; ++j) { - printf("[%s]: Running...\n", suite.tests[j].name); suite.tests[j].fn(); } } diff --git a/test/test.h b/test/test.h index e608256..ec1fa7f 100644 --- a/test/test.h +++ b/test/test.h @@ -10,27 +10,50 @@ #include -#define TEST_PASSED() printf("[%s]: Passed\n", __func__) -#define TEST(COND, ...) \ - do \ - { \ - bool cond = (COND); \ - if (!cond) \ - { \ - printf("\tFAIL: "); \ - } \ - else \ - { \ - printf("\tPASS: "); \ - } \ - printf("%s => ", #COND); \ - printf(__VA_ARGS__); \ - printf("\n"); \ - if (!cond) \ - { \ - assert(0); \ - } \ +#ifndef TEST_VERBOSE +#define TEST_VERBOSE 0 +#endif + +#define TEST_END() printf("\t[%s]: Passed\n", __func__) +#define TEST_INFO(...) \ + do \ + { \ + printf("\tINFO: "); \ + printf(__VA_ARGS__); \ + } while (0); + +#if TEST_VERBOSE +#define TEST_START() printf("\t[%s]: Running...\n", __func__) +#define TEST(COND, ...) \ + do \ + { \ + bool cond = (COND); \ + if (!cond) \ + { \ + printf("\t\tFAIL: "); \ + } \ + else \ + { \ + printf("\t\tPASS: "); \ + } \ + printf(__VA_ARGS__); \ + printf("\n\t\t [%s]\n", #COND); \ + if (!cond) \ + { \ + assert(0); \ + } \ } while (0) +#else +#define TEST_START() +#define TEST(COND, ...) \ + do \ + { \ + if (!(COND)) \ + { \ + assert(0); \ + } \ + } while (0) +#endif typedef struct TestFn { @@ -47,8 +70,13 @@ typedef struct const u64 size; } test_suite_t; -#define MAKE_TEST_SUITE(NAME) \ - {.name = #NAME, .tests = NAME, .size = ARRSIZE(NAME)} +#define MAKE_TEST_SUITE(NAME, DESC, ...) \ + const test_fn NAME##_TESTS[] = {__VA_ARGS__}; \ + const test_suite_t NAME = { \ + .name = DESC, \ + .tests = NAME##_TESTS, \ + .size = ARRSIZE(NAME##_TESTS), \ + } #endif diff --git a/test/test_lisp_api.c b/test/test_lisp_api.c index 0b02c30..ed23693 100644 --- a/test/test_lisp_api.c +++ b/test/test_lisp_api.c @@ -10,8 +10,11 @@ #include -void int_test(void) +void smi_test(void) { + TEST_START(); + // Standard old testing, checking both sides of the number line and our set + // bounds. i64 ints[] = { 1, -1, (1 << 10) - 1, (-1) * ((1 << 10) - 1), INT_MIN, INT_MAX, }; @@ -25,31 +28,41 @@ void int_test(void) TEST(in == out, "%ld == %ld", in, out); } - TEST_PASSED(); + TEST_END(); } -void symtable_test(void) +void smi_oob_test(void) { - sym_table_t table = {0}; - sym_table_init(&table); - for (u64 i = 0; i < ARRSIZE(words); ++i) - sym_table_find(&table, SV((char *)words[i], strlen(words[i]))); + TEST_START(); + // These are integers that are completely out of the bounds of our standard + // tagging system due to their size. We need to use big integers for this. + i64 ints[] = { + INT_MIN - 1, + INT_MAX + 1, + INT64_MIN, + INT64_MAX, + }; - TEST(table.count == ARRSIZE(unique_words), "%lu == %lu", table.count, - ARRSIZE(unique_words)); + for (u64 i = 0; i < ARRSIZE(ints); ++i) + { + i64 in = ints[i]; + lisp_t *lisp = make_int(in); + i64 out = as_int(lisp); - TEST(table.count < ARRSIZE(unique_words), "%lu < %lu", table.count, - ARRSIZE(unique_words)); + TEST(in != out, "%ld != %ld", in, out); + } - TEST_PASSED(); - - sym_table_cleanup(&table); + TEST_END(); } -void sym_test(void) +void sym_fresh_test(void) { + TEST_START(); sys_t system = {0}; sys_init(&system); + + // We expect every interned symbol to get a fresh allocation, but still be a + // valid representation of the original symbol. for (u64 i = 0; i < ARRSIZE(words); ++i) { const char *in = words[i]; @@ -57,14 +70,48 @@ void sym_test(void) char *out = as_sym(lisp); TEST(in != out, "%p != %p", in, out); TEST(strlen(in) == strlen(out), "%zu == %zu", strlen(in), strlen(out)); - TEST(strncmp(in, out, strlen(in)) == 0, "%d", strncmp(in, out, strlen(in))); + TEST(strncmp(in, out, strlen(in)) == 0, "`%s` == `%s`", in, out); } - TEST_PASSED(); + sys_free(&system); + TEST_END(); +} + +void sym_unique_test(void) +{ + TEST_START(); + sys_t system = {0}; + sys_init(&system); + + sv_t symbols[] = { + SV("hello", 5), + SV("goodbye", 7), + SV("display", 7), + SV("@xs'a_sh;d::a-h]", 16), + }; + + lisp_t *ptrs[ARRSIZE(symbols)]; + for (u64 i = 0; i < ARRSIZE(symbols); ++i) + { + ptrs[i] = intern(&system, symbols[i]); + TEST(ptrs[i] != 0, "%p (derived from `" PR_SV "`) is not NIL", + (void *)ptrs[i], SV_FMT(symbols[i])); + } + + for (u64 i = 0; i < ARRSIZE(symbols); ++i) + { + lisp_t *newptr = intern(&system, symbols[i]); + TEST(newptr == ptrs[i], "interning again (%p) gives us the same (%p)", + (void *)newptr, (void *)ptrs[i]); + } + + sys_free(&system); + TEST_END(); } void cons_test(void) { + TEST_START(); sys_t system = {0}; sys_init(&system); @@ -77,7 +124,13 @@ void cons_test(void) lisp = cons(&system, lword, lisp); } - // Make sure we've essentially reversed the `words` array + /* + As we've cons'd each word, we'd expect the order to be reversed. This test + will allow us to verify: + 1) words have actually been added to the linked list. + 2) words are in the order we expect. + in one go. + */ u64 i = ARRSIZE(words); for (lisp_t *iter = lisp; iter; iter = cdr(iter), --i) { @@ -89,22 +142,61 @@ void cons_test(void) TEST(strncmp(expected, got, size) == 0, "%s == %s", expected, got); } - TEST_PASSED(); - sys_free(&system); + TEST_END(); } -const test_fn TESTS_LISP_API[] = { - MAKE_TEST_FN(int_test), - MAKE_TEST_FN(sym_test), - MAKE_TEST_FN(cons_test), -}; +void sys_test(void) +{ + TEST_START(); + sys_t sys = {0}; + sys_init(&sys); + u64 old_memory_size = sys.memory.size; -const test_suite_t LISP_API_SUITE = { - .name = "Lisp API Tests", - .tests = TESTS_LISP_API, - .size = ARRSIZE(TESTS_LISP_API), -}; + // Creating integers doesn't affect memory size + (void)make_int(2000); + TEST(sys.memory.size == old_memory_size, + "Making integers doesn't affect system memory size"); + + // Creating symbols won't affect memory size, but does affect the symbol table + (void)intern(&sys, SV("hello world!", 12)); + TEST(sys.memory.size == old_memory_size, + "Interning doesn't affect system memory size"); + TEST(sys.symtable.count > 0, "Interning affects symbol table"); + + // Creating conses do affect memory size + (void)cons(&sys, make_int(1), make_int(2)); + TEST(sys.memory.size > 0, "Creating conses affects memory size"); + old_memory_size = sys.memory.size; + + (void)cons(&sys, intern(&sys, SV("test", 4)), NIL); + TEST(sys.memory.size > old_memory_size, + "Creating conses back to back affects memory size"); + old_memory_size = sys.memory.size; + + // Creating vectors does affect memory size + (void)make_vec(&sys, 8); + TEST(sys.memory.size > old_memory_size, + "Creating vectors (size 8) affects memory size"); + old_memory_size = sys.memory.size; + + (void)make_vec(&sys, 1000); + TEST(sys.memory.size > old_memory_size, + "Creating vectors (size 1000) affects memory size"); + old_memory_size = sys.memory.size; + + sys_free(&sys); + TEST(sys.memory.size == 0, "sys_free cleans up memory (shallow check)"); + TEST(sys.symtable.count == 0, "sys_free cleans up symtable (shallow check)"); + + TEST_END(); +} + +MAKE_TEST_SUITE(LISP_API_SUITE, "LISP API Tests", + + MAKE_TEST_FN(smi_test), MAKE_TEST_FN(smi_oob_test), + MAKE_TEST_FN(sym_fresh_test), MAKE_TEST_FN(sym_unique_test), + MAKE_TEST_FN(cons_test), MAKE_TEST_FN(sys_test), ); /* Copyright (C) 2026 Aryadev Chavali diff --git a/test/test_stream.c b/test/test_stream.c new file mode 100644 index 0000000..292fe02 --- /dev/null +++ b/test/test_stream.c @@ -0,0 +1,424 @@ +/* test_stream.c: Stream tests + * Created: 2026-02-05 + * Author: Aryadev Chavali + * License: See end of file + * Commentary: + */ + +#include +#include + +#include "./data.h" +#include "./test.h" + +#include +#include + +char valid_filename[50]; +FILE *valid_fp = NULL; +FILE *invalid_fp = NULL; + +void stream_test_prologue(void) +{ + const char filename_prefix[] = "build/stream_test_"; + valid_filename[ARRSIZE(valid_filename) - 1] = '\0'; + memcpy(valid_filename, filename_prefix, ARRSIZE(filename_prefix) - 1); + for (u64 i = ARRSIZE(filename_prefix) - 1; i < ARRSIZE(valid_filename) - 1; + ++i) + { + u8 num = (rand() % 36); + if (num < 26) + { + valid_filename[i] = num + 'a'; + } + else + { + valid_filename[i] = num + '0'; + } + } + + TEST_INFO("Creating file named `%.*s`\n", (int)ARRSIZE(valid_filename), + valid_filename); + valid_fp = fopen(valid_filename, "wb"); + // This should do a few things for us + // 1) Create a file, or clear the contents of it if it exists already. + // 2) Write some content to it. + assert(valid_fp); + fwrite(words_text, ARRSIZE(words_text) - 1, 1, valid_fp); + fclose(valid_fp); + valid_fp = fopen(valid_filename, "rb"); + assert(valid_fp); + + invalid_fp = NULL; +} + +void stream_test_epilogue(void) +{ + TEST_INFO("Freeing resources and deleting file `%s`\n", valid_filename); + assert(valid_fp); + fclose(valid_fp); + remove(valid_filename); +} + +void stream_test_string(void) +{ + TEST_START(); + sv_t test_strings[] = { + SV("hello, world!", 13), + SV("another string", 14), + SV((char *)text, ARRSIZE(text) / 2), + }; + + for (u64 i = 0; i < ARRSIZE(test_strings); ++i) + { + sv_t copy = sv_copy(test_strings[i]); + + stream_t stream = {0}; + stream_err_t err = stream_init_string(&stream, NULL, test_strings[i]); + TEST(err == STREAM_ERR_OK, "Stream initialising did not fail: %s", + stream_err_to_cstr(err)); + TEST(stream_size(&stream) == test_strings[i].size, + "Stream size is always string size (%lu == %lu)", stream_size(&stream), + test_strings[i].size); + TEST(!stream_eoc(&stream), "Not end of content already"); + + stream_stop(&stream); + TEST(strncmp(copy.data, test_strings[i].data, copy.size) == 0, + "Freeing a stream does not free the underlying memory it was derived " + "from"); + + free(copy.data); + } + + stream_t stream = {0}; + stream_err_t err = stream_init_string(&stream, NULL, SV(NULL, 0)); + TEST(err == STREAM_ERR_OK, "NULL stream initialising did not fail: %s", + stream_err_to_cstr(err)); + TEST(stream_size(&stream) == 0, "NULL stream size is 0"); + TEST(stream_eoc(&stream), "NULL stream is always at end of content"); + stream_stop(&stream); + + TEST_END(); +} + +void stream_test_file(void) +{ + TEST_START(); + + // Test that initialising works correctly + { + stream_t stream = {0}; + { + stream_err_t err = stream_init_file(&stream, valid_filename, valid_fp); + TEST(err == STREAM_ERR_OK, "Expected initialisating to be okay: %s", + stream_err_to_cstr(err)); + } + TEST(stream_size(&stream) == 0, "Stream doesn't read on init: size = %lu", + stream_size(&stream)); + TEST(!stream_eoc(&stream), "Stream should not be at the EoC from init."); + } + + // try to initialise the stream again but against a nonexistent file - we're + // expecting an error. + { + stream_t stream = {0}; + { + stream_err_t err = stream_init_file(&stream, NULL, invalid_fp); + TEST(err != STREAM_ERR_OK, "Expected initialisating to not be okay: %s", + stream_err_to_cstr(err)); + } + } + + TEST_END(); +} + +void stream_test_peek_next(void) +{ + TEST_START(); + + // Valid streams + { + stream_t stream = {0}; + stream_init_file(&stream, valid_filename, valid_fp); + + u64 old_position = stream.position; + char c1 = stream_peek(&stream); + TEST(c1 != '\0', "Peek should provide a normal character (%c)", c1); + TEST(stream.position == old_position, + "Peek should not shift the position (%lu -> %lu)", old_position, + stream.position); + + char c2 = stream_next(&stream); + TEST(c2 != '\0', "Next should provide a normal character (%c)", c2); + TEST(stream.position > old_position, + "Next should shift the position (%lu -> %lu)", old_position, + stream.position); + TEST(c2 != c1, + "Next should yield a different character (%c) to the previous peek " + "(%c)", + c2, c1); + + char c3 = stream_peek(&stream); + TEST(c3 == c2, + "Peeking should yield the same character (%c) as the previous next " + "(%c)", + c3, c2); + + stream_stop(&stream); + } + + // Invalid streams + { + stream_t stream = {0}; + stream_init_file(&stream, NULL, invalid_fp); + char c = stream_peek(&stream); + TEST(c == '\0', "Invalid streams should have an invalid peek (%c)", c); + + u64 old_position = stream.position; + c = stream_next(&stream); + TEST(c == '\0', "Invalid streams should have an invalid next (%c)", c); + TEST(old_position == stream.position, + "Next on an invalid stream should not affect position (%lu -> %lu)", + old_position, stream.position); + + stream_stop(&stream); + } + TEST_END(); +} + +void stream_test_seek(void) +{ + TEST_START(); + // Seeking on invalid streams + { + stream_t stream = {0}; + stream_init_file(&stream, NULL, invalid_fp); + + u64 old_position = stream.position; + TEST(!stream_seek_forward(&stream, 1), + "Shouldn't be possible to seek forward on an invalid stream."); + TEST(old_position == stream.position, + "Position shouldn't be affected when seeking forward on an invalid " + "stream" + "(%lu -> %lu)", + old_position, stream.position); + + TEST(!stream_seek_backward(&stream, 1), + "Shouldn't be possible to seek backward on an invalid stream."); + TEST(old_position == stream.position, + "Position shouldn't be affected when seeking backward on an invalid " + "stream (%lu -> %lu)", + old_position, stream.position); + + stream_stop(&stream); + } + + // Valid streams + { + stream_t stream = {0}; + stream_init_file(&stream, valid_filename, valid_fp); + + u64 old_position = stream.position; + TEST(stream_seek_forward(&stream, 1), + "Okay to seek forward on a valid stream."); + TEST(old_position < stream.position, + "Position should be greater than before when seeking forward on a " + "valid stream (%lu -> %lu)", + old_position, stream.position); + + TEST(stream_seek_backward(&stream, 1), + "Okay to seek backward on a valid stream."); + TEST(old_position == stream.position, + "stream_seek_forward and stream_seek_backward are inverse operations"); + + u64 forward_offset = stream_seek_forward(&stream, ARRSIZE(words_text) * 2); + TEST(forward_offset < ARRSIZE(words_text) * 2, + "Forward seeking by offsets greater than file size clamps (%lu " + "clamps to %lu)", + ARRSIZE(words_text) * 2, forward_offset); + + u64 file_size = stream_size(&stream); + u64 backward_offset = stream_seek_backward(&stream, file_size + 1); + TEST(backward_offset == file_size, + "Backward seeking by offsets greater than file size clamps (%lu " + "clamps to %lu)", + file_size + 1, backward_offset); + + TEST(stream.position == 0, + "Clamped forward and clamped backward seeking " + "leads to start of stream (position=%lu)", + stream.position); + + i64 r_forward_offset = (rand() % (file_size - 1)) + 1; + i64 r_backward_offset = (rand() % (file_size - 1)) + 1; + while (r_backward_offset >= r_forward_offset) + r_backward_offset = (rand() % (file_size - 1)) + 1; + + TEST(stream_seek(&stream, r_forward_offset) == (u64)r_forward_offset, + "Seeking by a random positive offset (%lu) is valid", + r_forward_offset); + + TEST(stream_seek(&stream, -r_backward_offset) == (u64)r_backward_offset, + "Seeking backward by a random negative offset (%lu) is valid", + r_backward_offset); + + TEST( + (i64)stream.position == r_forward_offset - r_backward_offset, + "Stream position (%lu) is exactly shifted by seeking offsets described " + "above.", + stream.position); + + stream_stop(&stream); + } + + TEST_END(); +} + +void stream_test_substr(void) +{ + TEST_START(); + u64 size = rand() % (ARRSIZE(words_text) - 1); + u64 position = ARRSIZE(words_text) - size - 1; + + // Taking substrings of invalid streams + { + stream_t stream = {0}; + stream_init_file(&stream, NULL, invalid_fp); + + // Relative + { + sv_t result = stream_substr(&stream, size); + TEST(result.data == NULL && result.size == 0, + "Relative substring with size %lu on invalid stream should be NULL", + size); + } + + // Absolute + { + sv_t result = stream_substr_abs(&stream, position, size); + TEST(result.data == NULL && result.size == 0, + "Absolute substring @%lu with size %lu on invalid stream should be " + "NULL", + position, size); + } + + stream_stop(&stream); + } + + // Taking substrings of valid streams + { + stream_t stream = {0}; + stream_init_file(&stream, valid_filename, valid_fp); + + // Absolute + { + sv_t result = stream_substr_abs(&stream, position, size); + TEST(result.data && result.size, + "Absolute substring @%lu with size %lu on valid stream should be " + "nonzero", + position, size); + + TEST(result.size == size, "Substring has right size (%lu)", result.size); + + sv_t expected = SV((char *)words_text + position, size); + TEST(strncmp(result.data, expected.data, result.size) == 0, + "Expect the substring to be the same as the data we put in"); + } + + // Relative + { + sv_t result = stream_substr(&stream, size); + TEST(result.data && result.size, + "Relative substring with size %lu should be nonzero", size); + + TEST(result.size == size, "Substring has right size (%lu)", result.size); + + sv_t expected = SV((char *)words_text, size); + TEST(strncmp(result.data, expected.data, result.size) == 0, + "Expect the substring to be the same as the data we put in"); + } + + // Relative substring after seeking + { + // Shift forward to a random position + assert(stream_seek_forward(&stream, position)); // not a test + sv_t result = stream_substr(&stream, size); + TEST(result.data && result.size, + "Relative substring with size %lu after seeking %lu bytes should be " + "nonzero", + size, position); + + TEST(result.size == size, "Substring has right size (%lu)", result.size); + + sv_t expected = SV((char *)words_text + position, size); + TEST(strncmp(result.data, expected.data, result.size) == 0, + "Expect the substring to be the same as the data we put in"); + + // Shift back to the original position. + assert(stream_seek_backward(&stream, position)); // not a test + } + + // Bad substrings + { + { + sv_t result = stream_substr_abs(&stream, stream_size(&stream), 100); + TEST(!result.data && !result.size, + "Absolute substring at %lu of 100 bytes is invalid", + stream_size(&stream)); + } + + assert(stream_seek_forward(&stream, stream_size(&stream))); // not a test + { + sv_t result = stream_substr(&stream, 100); + TEST(!result.data && !result.size, + "Relative substring with size 100 after seeking %lu bytes is " + "invalid", + stream.position); + } + } + + stream_stop(&stream); + } + TEST_END(); +} + +void stream_test_till(void) +{ + TEST_START(); + TODO("Not implemented"); +} + +void stream_test_while(void) +{ + TEST_START(); + TODO("Not implemented"); +} + +void stream_test_line_col(void) +{ + TEST_START(); + TODO("Not implemented"); +} + +MAKE_TEST_SUITE(STREAM_SUITE, "Stream Tests", + + MAKE_TEST_FN(stream_test_prologue), + MAKE_TEST_FN(stream_test_string), + MAKE_TEST_FN(stream_test_file), + MAKE_TEST_FN(stream_test_peek_next), + MAKE_TEST_FN(stream_test_seek), + MAKE_TEST_FN(stream_test_substr), + MAKE_TEST_FN(stream_test_epilogue), ); + +/* Copyright (C) 2026 Aryadev Chavali + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for + * details. + + * You may distribute and modify this code under the terms of the GNU General + * Public License Version 2, which you should have received a copy of along with + * this program. If not, please go to . + + */ diff --git a/test/test_sv.c b/test/test_sv.c new file mode 100644 index 0000000..ec6b6cf --- /dev/null +++ b/test/test_sv.c @@ -0,0 +1,45 @@ +/* test_sv.c: String View tests + * Created: 2026-02-05 + * Author: Aryadev Chavali + * License: See end of file + * Commentary: + */ + +#include +#include + +#include "./data.h" +#include "./test.h" + +void sv_copy_test(void) +{ + TEST_START(); + static_assert(ARRSIZE(unique_words) > 3, "Expected at least 3 unique words"); + for (u64 i = 0; i < 3; ++i) + { + sv_t word = SV((char *)unique_words[i], strlen(unique_words[i])); + sv_t copy = sv_copy(word); + TEST(word.data != copy.data, "%p != %p", word.data, copy.data); + TEST(word.size == copy.size, "%lu == %lu", word.size, copy.size); + TEST(strncmp(word.data, copy.data, copy.size) == 0, "`%s` == `%s`", + word.data, copy.data); + + // Obviously we can't just have this lying around. + free(copy.data); + } +} + +MAKE_TEST_SUITE(SV_SUITE, "String View Tests", MAKE_TEST_FN(sv_copy_test), ); + +/* Copyright (C) 2026 Aryadev Chavali + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for + * details. + + * You may distribute and modify this code under the terms of the GNU General + * Public License Version 2, which you should have received a copy of along with + * this program. If not, please go to . + + */ diff --git a/test/test_symtable.c b/test/test_symtable.c new file mode 100644 index 0000000..cf640a9 --- /dev/null +++ b/test/test_symtable.c @@ -0,0 +1,41 @@ +/* test_symtable.c: Symbol table tests + * Created: 2026-02-05 + * Author: Aryadev Chavali + * License: See end of file + * Commentary: + */ + +#include "./data.h" +#include "./test.h" + +void symtable_test(void) +{ + TEST_START(); + sym_table_t table = {0}; + sym_table_init(&table); + for (u64 i = 0; i < ARRSIZE(words); ++i) + sym_table_find(&table, SV((char *)words[i], strlen(words[i]))); + + TEST(table.count == ARRSIZE(unique_words), "%lu == %lu", table.count, + ARRSIZE(unique_words)); + + sym_table_free(&table); + TEST_END(); +} + +MAKE_TEST_SUITE(SYMTABLE_SUITE, "Symbol Table Tests", + + MAKE_TEST_FN(symtable_test), ); + +/* Copyright (C) 2026 Aryadev Chavali + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for + * details. + + * You may distribute and modify this code under the terms of the GNU General + * Public License Version 2, which you should have received a copy of along with + * this program. If not, please go to . + + */ diff --git a/test/test_vec.c b/test/test_vec.c index 6e901ea..0b8300e 100644 --- a/test/test_vec.c +++ b/test/test_vec.c @@ -8,8 +8,9 @@ #include "./data.h" #include "./test.h" -void vec_test1(void) +void vec_test_concat(void) { + TEST_START(); sys_t system = {0}; sys_init(&system); @@ -28,15 +29,17 @@ void vec_test1(void) TEST(vec->size == ARRSIZE(words_text), "%lu == %lu", vec->size, ARRSIZE(words_text)); - TEST(strncmp((char *)vec_data(vec), words_text, vec->size) == 0, "%d", - strncmp((char *)vec_data(vec), words_text, vec->size)); + TEST(strncmp((char *)vec_data(vec), words_text, vec->size) == 0, + "%p@%lu == %p@%lu", (char *)vec_data(vec), vec->size, words_text, + strlen(words_text)); - TEST_PASSED(); sys_free(&system); + TEST_END(); } -void vec_test2(void) +void vec_test_gen_substr(void) { + TEST_START(); sys_t system = {0}; sys_init(&system); // Generating substrings @@ -60,24 +63,18 @@ void vec_test2(void) vec_append(as_vec(lvec), text + test.start, test.size); TEST(as_vec(lvec)->size > size, "%lu > %lu", as_vec(lvec)->size, size); TEST(strncmp((char *)vec_data(as_vec(lvec)), substr.data, substr.size) == 0, - "%d", - strncmp((char *)vec_data(as_vec(lvec)), substr.data, substr.size)); + "%p@%lu == %p@%lu", (char *)vec_data(as_vec(lvec)), as_vec(lvec)->size, + substr.data, substr.size); } - TEST_PASSED(); sys_free(&system); + TEST_END(); } -const test_fn TESTS_VEC[] = { - MAKE_TEST_FN(vec_test1), - MAKE_TEST_FN(vec_test2), -}; +MAKE_TEST_SUITE(VEC_SUITE, "Vector Tests", -const test_suite_t VEC_SUITE = { - .name = "Vector Tests", - .tests = TESTS_VEC, - .size = ARRSIZE(TESTS_VEC), -}; + MAKE_TEST_FN(vec_test_concat), + MAKE_TEST_FN(vec_test_gen_substr), ); /* Copyright (C) 2026 Aryadev Chavali