Compare commits

..

12 Commits

Author SHA1 Message Date
seeseemelk ec896495a3 Fix infinite loop bug 2026-04-29 14:40:06 +02:00
seeseemelk eb4b0495f2 Working on parser refactor 2026-04-29 14:36:42 +02:00
seeseemelk 1f40c8f5ee Refactor tests a bit more 2026-04-29 13:25:41 +02:00
seeseemelk 98d58a2169 Refactor tests 2026-04-29 13:09:14 +02:00
seeseemelk f0621a8076 Refactor parser 2026-04-29 11:53:26 +02:00
seeseemelk 84747028f5 Ensure alias and import can be mixed 2026-04-29 11:46:02 +02:00
seeseemelk f90cad2b96 Use proper public keyword 2026-04-29 11:43:14 +02:00
seeseemelk e09bd72441 Update ast interface 2026-04-29 11:24:42 +02:00
seeseemelk 9035cc639c Add alias to ast 2026-04-29 11:18:40 +02:00
seeseemelk 3288efdfd7 Refactor test interface 2026-04-29 10:59:06 +02:00
seeseemelk 34b7939f76 Refactor parser to C11 and update build configuration 2026-04-29 10:38:34 +02:00
seeseemelk 15714393c3 Refactor parser to use Token in AST and update tests 2026-04-29 10:35:12 +02:00
29 changed files with 590 additions and 265 deletions
+5
View File
@@ -40,3 +40,8 @@ the agent to update the implementation.
When creating a commit, make sure that both the user's and the agent's modifications When creating a commit, make sure that both the user's and the agent's modifications
are included in the commit. are included in the commit.
Only create a commit when specifically asked for that. Never assume implicitly that the
user wants you to create a commit.
Even if they asked you to create a commit in an earlier task, it does not mean that
you should also create a commit in a later task.
+60 -7
View File
@@ -5,29 +5,82 @@
#define AST_H #define AST_H
#include "bool.h" #include "bool.h"
#include "token.h"
#include <stddef.h> #include <stddef.h>
typedef struct { typedef struct {
/* @brief The name of the module being imported. */ /** @brief The name of the module being imported. */
char* module_name; const char* module_name;
/* @brief Whether the import is public or not. */ /** @brief Whether the import is public or not. */
bool is_public; bool is_public;
} ImportDeclaration; } ImportDeclaration;
typedef enum {
TYPE_EXPRESSION_BUILTIN,
TYPE_EXPRESSION_ARRAY
} TypeExpressionTag;
/**
* An expression that evaluates to a type.
*/
typedef struct TypeExpression TypeExpression;
struct TypeExpression{
/** @brief defines which entry in the union is valid */
TypeExpressionTag tag;
union {
/** @brief Evaluates to an array of the given type. */
struct {
/** @brief A pointer to the type of the elements stored in the array. */
TypeExpression* array;
} array;
/** @brief Evaluates to a builtin integer type.*/
struct {
/**
* @brief The number of bits in the integer.
* Typical values are 8, 16, 32, and 64.
*/
int bitSize;
/** @brief `true` if the type is signed, `false` if it's unsigned. */
bool isSigned;
} builtin;
};
};
/**
* A declaration that aliases one type to another.
*/
typedef struct {
/** @brief The name of the alias. */
const char* name;
/** @brief The value of the alias. */
TypeExpression value;
/** @brief Whether the import is public or not. */
bool is_public;
} AliasDeclaration;
/** /**
* The top-level model. * The top-level model.
* Every file matches an entire Module. * Every file matches an entire Module.
*/ */
typedef struct { typedef struct {
/* @brief The name of the module. */ /** @brief The name of the module. */
char* name; const char* name;
/* @brief The list of imports in the module. */ /** @brief The list of imports in the module. */
ImportDeclaration* imports; ImportDeclaration* imports;
/* @brief The number of imports in the module. */ /** @brief The number of imports in the module. */
size_t import_count; size_t import_count;
/** @brief The list of aliases in the module. */
AliasDeclaration* aliases;
/** @brief The number of aliases in the module. */
size_t alias_count;
} Module; } Module;
#endif #endif
+2 -2
View File
@@ -1,4 +1,4 @@
V0_SRC := v0/main.c v0/util.c v0/token.c v0/parser.c v0/log.c V0_SRC := v0/main.c v0/util.c v0/token.c v0/parser.c v0/log.c v0/str.c
# V0_TEST must only include `v0/test.c` itself, as all other test Csource files are # V0_TEST must only include `v0/test.c` itself, as all other test Csource files are
# included directly into `v0/test.c` using `#include "test_xyz.c"`. # included directly into `v0/test.c` using `#include "test_xyz.c"`.
@@ -11,7 +11,7 @@ V0_TEST_OBJ := $(patsubst v0/%.c,v0/bin/%.o,$(V0_TEST))
V0_SRC_DEPS := $(V0_SRC_OBJ:.o=.d) V0_SRC_DEPS := $(V0_SRC_OBJ:.o=.d)
V0_TEST_DEPS := $(V0_TEST_OBJ:.o=.d) V0_TEST_DEPS := $(V0_TEST_OBJ:.o=.d)
CFLAGS += -Werror -Wall -pedantic -std=c89 CFLAGS += -Werror -Wall -pedantic -std=c11
v0/bin/c2: $(V0_SRC_OBJ) v0/bin/c2: $(V0_SRC_OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(CC) $(CFLAGS) -o $@ $^
+2 -2
View File
@@ -20,7 +20,7 @@ void log_error(const char* msg) {
} }
} }
void log_on_line(Location* loc, int to_column, const char* msg, ...) { void log_on_line(Location* loc, const char* msg, ...) {
/* Declarations first for C89 */ /* Declarations first for C89 */
char* line_prefix = NULL; char* line_prefix = NULL;
char* formatted_msg = NULL; char* formatted_msg = NULL;
@@ -36,7 +36,7 @@ void log_on_line(Location* loc, int to_column, const char* msg, ...) {
line_prefix = format_string("%d| ", loc->line); line_prefix = format_string("%d| ", loc->line);
if (!line_prefix) goto cleanup; if (!line_prefix) goto cleanup;
caret_len = to_column - loc->column_start + 1; caret_len = loc->column_end - loc->column_start + 1;
if (caret_len < 1) caret_len = 1; if (caret_len < 1) caret_len = 1;
/* Format the message */ /* Format the message */
+1 -2
View File
@@ -28,10 +28,9 @@ void log_error(const char* msg);
* It additionally supports the `%S` format specifier, which can be used to format a `String` structure from `string.h`. * It additionally supports the `%S` format specifier, which can be used to format a `String` structure from `string.h`.
* *
* @param loc The location where the error occurred. * @param loc The location where the error occurred.
* @param to_column The column number where the error ends.
* @param msg The error message to log. This can contain format specifiers like printf, and the additional arguments will be formatted into the message. * @param msg The error message to log. This can contain format specifiers like printf, and the additional arguments will be formatted into the message.
* @param ... Additional arguments to format into the error message. * @param ... Additional arguments to format into the error message.
*/ */
void log_on_line(Location* loc, int to_column, const char* msg, ...); void log_on_line(Location* loc, const char* msg, ...);
#endif #endif
+245 -74
View File
@@ -4,110 +4,281 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
Module* parser_parse(TokenStream* ts) { // /**
/* Declarations first for C89 */ // * Parses an import declaration.
Token t; // *
Module* module; // * @param ts The token stream to parse from.
ImportDeclaration* new_imports; // * @param module The module being parsed.
int is_public; // * @returns true on success, false on failure.
// */
// static bool parse_import(TokenStream* ts, Module* module) {
// ImportDeclaration* new_imports = realloc(module->imports, (module->import_count + 1) * sizeof(ImportDeclaration));
// if (!new_imports) {
// fprintf(stderr, "Out of memory\n");
// exit(1);
// }
// module->imports = new_imports;
t = tokenstream_next(ts); // Token t = tokenstream_next(ts);
if (t.token != TOKEN_MODULE) { // bool is_public = false;
log_on_line(&t.location, t.location.column_end, "expected 'module' keyword"); // if (t.token == TOKEN_PUBLIC) {
return NULL; // is_public = true;
// t = tokenstream_next(ts);
// }
// if (t.token != TOKEN_IDENTIFIER) {
// log_on_line(&t.location, t.location.column_end, "expected module name to import");
// return false;
// }
// char* name = (char*)malloc(t.text.length + 1);
// memcpy(name, t.text.data, t.text.length);
// name[t.text.length] = '\0';
// module->imports[module->import_count] = (ImportDeclaration){ .module_name = name, .is_public = is_public };
// module->import_count++;
// t = tokenstream_next(ts);
// if (t.token != TOKEN_SEMICOLON) {
// log_on_line(&t.location, t.location.column_end, "expected ';' after import");
// return false;
// }
// return true;
// }
// /**
// * Parses an alias declaration.
// *
// * @param ts The token stream to parse from.
// * @param module The module being parsed.
// * @returns true on success, false on failure.
// */
// static bool parse_alias(TokenStream* ts, Module* module) {
// AliasDeclaration* new_aliases = realloc(module->aliases, (module->alias_count + 1) * sizeof(AliasDeclaration));
// if (!new_aliases) {
// fprintf(stderr, "Out of memory\n");
// exit(1);
// }
// module->aliases = new_aliases;
// Token t = tokenstream_next(ts);
// if (t.token != TOKEN_IDENTIFIER) {
// log_on_line(&t.location, t.location.column_end, "expected alias name");
// return false;
// }
// char* name = (char*)malloc(t.text.length + 1);
// memcpy(name, t.text.data, t.text.length);
// name[t.text.length] = '\0';
// AliasDeclaration alias;
// alias.name = name;
// t = tokenstream_next(ts);
// if (t.token != TOKEN_ASSIGN) {
// log_on_line(&t.location, t.location.column_end, "expected '='");
// return false;
// }
// t = tokenstream_next(ts);
// TypeExpression type;
// if (t.token == TOKEN_IDENTIFIER && strncmp(t.text.data, "int32", t.text.length) == 0) {
// type = (TypeExpression){ .tag = TYPE_EXPRESSION_BUILTIN, .builtin = { .bitSize = 32, .isSigned = true } };
// t = tokenstream_next(ts);
// if (t.token == TOKEN_BRACKET_OPEN) {
// t = tokenstream_next(ts);
// if (t.token != TOKEN_BRACKET_CLOSE) {
// log_on_line(&t.location, t.location.column_end, "expected ']'");
// return false;
// }
// TypeExpression* inner = malloc(sizeof(TypeExpression));
// *inner = type;
// type = (TypeExpression){ .tag = TYPE_EXPRESSION_ARRAY, .array = { .array = inner } };
// t = tokenstream_next(ts);
// }
// } else {
// log_on_line(&t.location, t.location.column_end, "expected type");
// return false;
// }
// alias.value = type;
// module->aliases[module->alias_count] = alias;
// module->alias_count++;
// if (t.token != TOKEN_SEMICOLON) {
// log_on_line(&t.location, t.location.column_end, "expected ';'");
// return false;
// }
// return true;
// }
typedef struct {
TokenStream* ts;
Token token;
} Parser;
/**
* Reads a new token into p->token.
*/
static void parser_next_token(Parser* p) {
p->token = tokenstream_next(p->ts);
} }
t = tokenstream_next(ts); /**
if (t.token != TOKEN_IDENTIFIER) { * Reads a new token if the current token is equal to the expected token.
log_on_line(&t.location, t.location.column_end, "expected module name"); *
return NULL; * If they are equal, it continues to the next token.
*
* @param p
* @param token The expected token.
* @returns `true` if the current token matches the expected, `false` if it does not.
*/
static bool parser_accept(Parser* p, TokenType token) {
if (p->token.token == token) {
parser_next_token(p);
return true;
}
return false;
} }
module = (Module*)malloc(sizeof(Module)); /**
if (module == NULL) { * @copilot todo
fprintf(stderr, "Out of memory\n"); */
exit(1); static bool parser_expect(Parser* p, TokenType token, const char* msg) {
if (parser_accept(p, token)) {
return true;
} }
module->name = NULL; log_on_line(&p->token.location, msg);
module->imports = NULL; return false;
module->import_count = 0;
module->name = (char*)malloc(t.text.length + 1);
if (module->name == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
} }
memcpy(module->name, t.text.data, t.text.length); /**
module->name[t.text.length] = '\0'; * @copilot todo add docs
*/
t = tokenstream_next(ts); static bool parser_peek(Parser* p, TokenType token) {
if (t.token != TOKEN_SEMICOLON) { if (p->token.token == token) {
log_on_line(&t.location, t.location.column_end, "expected ';' after module name"); return true;
parser_free(module); }
return NULL; return false;
} }
module->imports = NULL; /**
module->import_count = 0; * @copilot todo add docs
*/
while (1) { static bool parser_require(Parser* p, TokenType token, const char* msg) {
t = tokenstream_next(ts); if (parser_peek(p, token)) {
if (t.token != TOKEN_IMPORT) { return true;
break; }
log_on_line(&p->token.location, msg);
return false;
} }
new_imports = realloc(module->imports, (module->import_count + 1) * sizeof(ImportDeclaration)); /**
if (!new_imports) { * Converts the current token to a string.
fprintf(stderr, "Out of memory\n"); * @copilot add proper docs
exit(1); */
} static char* parser_to_text(Parser* p) {
module->imports = new_imports; char* str = string_copy(p->token.text);
parser_next_token(p);
t = tokenstream_next(ts); return str;
is_public = 0;
if (t.token == TOKEN_IDENTIFIER && strncmp(t.text.data, "public", t.text.length) == 0) {
is_public = 1;
t = tokenstream_next(ts);
} }
if (t.token != TOKEN_IDENTIFIER) { /**
log_on_line(&t.location, t.location.column_end, "expected module name to import"); * Parses the "module" keyword
parser_free(module); */
return NULL; static bool parse_declaration_module(Parser* p, Module* module) {
if (!parser_expect(p, TOKEN_MODULE, "expected keyword 'module'")) {
return false;
} }
module->imports[module->import_count].module_name = (char*)malloc(t.text.length + 1); if (!parser_require(p, TOKEN_IDENTIFIER, "expected module identifier")) {
if (!module->imports[module->import_count].module_name) { return false;
fprintf(stderr, "Out of memory\n");
exit(1);
} }
memcpy(module->imports[module->import_count].module_name, t.text.data, t.text.length); module->name = parser_to_text(p);
module->imports[module->import_count].module_name[t.text.length] = '\0';
module->imports[module->import_count].is_public = is_public; return parser_expect(p, TOKEN_SEMICOLON, "expected ';' after module name");
}
static bool parse_import(Parser* p, Module* module, bool is_public) {
module->import_count++; module->import_count++;
module->imports = realloc(module->imports, sizeof(ImportDeclaration) * module->import_count);
ImportDeclaration* import = &module->imports[module->import_count - 1];
t = tokenstream_next(ts); if (!parser_expect(p, TOKEN_IDENTIFIER, "expected module identifier")) {
if (t.token != TOKEN_SEMICOLON) { return false;
log_on_line(&t.location, t.location.column_end, "expected ';' after import");
parser_free(module);
return NULL;
} }
import->module_name = parser_to_text(p);
import->is_public = is_public;
return true;
}
static bool parse_alias(Parser* p, Module* module, bool is_public) {
// @copilot implement
return true;
}
Module* parser_parse(TokenStream* ts) {
Parser* p = malloc(sizeof(Parser));
p->ts = ts;
parser_next_token(p);
Module* module = malloc(sizeof(Module));
if (!parse_declaration_module(p, module)) {
goto fail;
}
while (!parser_peek(p, TOKEN_EOF)) {
bool is_public = false;
bool terminal = false;
do {
if (parser_accept(p, TOKEN_IMPORT)) {
if (!parse_import(p, module, is_public)) {
goto fail;
}
terminal = true;
} else if (parser_accept(p, TOKEN_ALIAS)) {
if (!parse_alias(p, module, is_public)) {
goto fail;
}
terminal = true;
} else if (parser_accept(p, TOKEN_PUBLIC)) {
is_public = true;
} else {
log_on_line(&p->token.location, "unexpected token");
goto fail;
}
} while (!terminal);
} }
return module; return module;
fail:
free(module);
return NULL;
}
void free_type_expression(TypeExpression* expr) {
if (expr->tag == TYPE_EXPRESSION_ARRAY) {
free_type_expression(expr->array.array);
free(expr->array.array);
}
} }
void parser_free(Module* module) { void parser_free(Module* module) {
if (module == NULL) return; if (module == NULL) return;
if (module->imports != NULL) { if (module->imports != NULL) {
/* C89: declare index variable before the loop */ for(size_t i = 0; i < module->import_count; i++) {
size_t i; free((void*)module->imports[i].module_name);
for (i = 0; i < module->import_count; i++) {
free(module->imports[i].module_name);
} }
free(module->imports); free(module->imports);
} }
free(module->name); if (module->aliases != NULL) {
for(size_t i = 0; i < module->alias_count; i++) {
free((void*)module->aliases[i].name);
free_type_expression(&module->aliases[i].value);
}
free(module->aliases);
}
free((void*)module->name);
free(module); free(module);
} }
+11
View File
@@ -0,0 +1,11 @@
#include "str.h"
#include <string.h>
#include <stdlib.h>
char* string_copy(String string) {
char* str = malloc(string.length + 1);
memcpy(str, string.data, string.length);
str[string.length] = '\0';
return str;
}
+10
View File
@@ -14,4 +14,14 @@ typedef struct {
size_t length; size_t length;
} String; } String;
/**
* Creates a copy of a string.
*
* Note that this copy has to be freed afterwards.
*
* @param string The string to copy.
* @returns A null-terminated copy of the string.
*/
char* string_copy(String string);
#endif #endif
+100 -65
View File
@@ -1,9 +1,12 @@
#include "test.h" #include "test.h"
#include "util.h"
#include "parser.h"
#include <setjmp.h> #include <setjmp.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include "util.h"
static jmp_buf s_testJmp; static jmp_buf s_testJmp;
static const char* s_failMsg; static const char* s_failMsg;
@@ -11,27 +14,14 @@ static char* s_logOutput = NULL;
static const char* s_currentTestName = NULL; static const char* s_currentTestName = NULL;
static char* s_testSource = NULL; static char* s_testSource = NULL;
static Module* s_currentModule = NULL;
static TokenStream* s_currentTokenStream = NULL;
void fail(const char* msg) { void fail(const char* msg) {
s_failMsg = msg; s_failMsg = msg;
longjmp(s_testJmp, 1); longjmp(s_testJmp, 1);
} }
void assert_not_null(void* ptr, const char* msg) {
if (ptr == NULL) {
fail(msg);
}
}
void assert_str(const char* expected, const char* actual, const char* msg) {
if (expected == NULL || actual == NULL || strcmp(expected, actual) != 0) {
fail(msg);
}
}
void assert_log(const char* expected, const char* msg) {
assert_str(expected, s_logOutput, msg);
}
char* read_file_content(const char* filepath) { char* read_file_content(const char* filepath) {
FILE* f; FILE* f;
long size; long size;
@@ -53,6 +43,59 @@ char* read_file_content(const char* filepath) {
return content; return content;
} }
void assert_not_null(void* ptr, const char* msg) {
if (ptr == NULL) {
fail(msg);
}
}
void assert_string(const char* expected, String actual, const char* msg) {
if (expected == NULL || actual.data == NULL || strlen(expected) != actual.length || strncmp(expected, actual.data, actual.length) != 0) {
fail(msg);
}
}
void assert_str(const char* expected, const char* actual, const char* msg) {
if (expected == NULL || actual == NULL || strcmp(expected, actual) != 0) {
fail(msg);
}
}
TokenStream* test_get_tokenstream(void) {
if (s_currentTokenStream == NULL) {
char* filepath = NULL;
filepath = format_string("v0/tests/%s.c2", s_currentTestName);
if (!filepath) {
fail("out of memory");
return NULL;
}
if (s_testSource) free(s_testSource);
s_testSource = read_file_content(filepath);
if (!s_testSource) {
puts(filepath);
fail("could not read test source file");
free(filepath);
return NULL;
}
s_currentTokenStream = tokenstream_open(filepath, s_testSource);
free(filepath);
}
return s_currentTokenStream;
}
Module* test_get_ast(void) {
if (s_currentModule == NULL) {
s_currentModule = parser_parse(test_get_tokenstream());
}
return s_currentModule;
}
void assert_log(const char* expected, const char* msg) {
assert_str(expected, s_logOutput, msg);
}
void assert_log_file(const char* msg) { void assert_log_file(const char* msg) {
char* filepath = format_string("v0/tests/%s.log", s_currentTestName); char* filepath = format_string("v0/tests/%s.log", s_currentTestName);
const char* generate; const char* generate;
@@ -109,28 +152,6 @@ void assert_false(bool condition, const char* msg) {
} }
} }
TokenStream* tokenstream_get_test(void) {
char* filepath = NULL;
TokenStream* ts = NULL;
filepath = format_string("v0/tests/%s.c2", s_currentTestName);
if (!filepath) {
fail("out of memory");
return NULL;
}
if (s_testSource) free(s_testSource);
s_testSource = read_file_content(filepath);
if (!s_testSource) {
fail("could not read test source file");
free(filepath);
return NULL;
}
ts = tokenstream_open(filepath, s_testSource);
free(filepath);
return ts;
}
static void log_append(const char* msg) { static void log_append(const char* msg) {
size_t oldLen = s_logOutput ? strlen(s_logOutput) : 0; size_t oldLen = s_logOutput ? strlen(s_logOutput) : 0;
size_t newLen = oldLen + strlen(msg) + 1; size_t newLen = oldLen + strlen(msg) + 1;
@@ -164,35 +185,37 @@ typedef struct {
static int s_totalTests; static int s_totalTests;
static int s_greenTests; static int s_greenTests;
#define TEST(name) {#name, name},
static TestCase s_tests[] = { static TestCase s_tests[] = {
{"tokenstream_open_fail", test_tokenstream_open_fail}, TEST(test_log_error)
{"tokenstream_simple_keyword", test_tokenstream_simple_keyword}, TEST(test_log_on_line_variadic)
{"tokenstream_keywords_and_symbols", test_tokenstream_keywords_and_symbols}, TEST(test_log_on_line)
{"tokenstream_parentheses_and_brackets", test_tokenstream_parentheses_and_brackets}, TEST(test_parser_alias_and_import_mix)
{"tokenstream_comma", test_tokenstream_comma}, TEST(test_parser_alias_array)
{"tokenstream_whitespace_ignored", test_tokenstream_whitespace_ignored}, TEST(test_parser_alias_simple)
{"tokenstream_void_function_signature", test_tokenstream_void_function_signature}, TEST(test_parser_bad_import_name)
{"tokenstream_unknown_token", test_tokenstream_unknown_token}, TEST(test_parser_bad_module_name)
{"tokenstream_info", test_tokenstream_info}, TEST(test_parser_imports)
{"parser_module_name", test_parser_module_name}, TEST(test_parser_missing_semicolon_import)
{"parser_bad_module_name", test_parser_bad_module_name}, TEST(test_parser_missing_semicolon_module)
{"parser_missing_semicolon_module", test_parser_missing_semicolon_module}, TEST(test_parser_module_name)
{"parser_missing_semicolon_import", test_parser_missing_semicolon_import}, TEST(test_parser_public_imports)
{"parser_bad_import_name", test_parser_bad_import_name}, TEST(test_tokenstream_comma)
{"parser_imports", test_parser_imports}, TEST(test_tokenstream_info)
{"parser_public_imports", test_parser_public_imports}, TEST(test_tokenstream_keywords_and_symbols)
{"log_error", test_log_error}, TEST(test_tokenstream_open_fail)
{"log_on_line", test_log_on_line}, TEST(test_tokenstream_parentheses_and_brackets)
{"log_on_line_variadic", test_log_on_line_variadic}, TEST(test_tokenstream_simple_keyword)
TEST(test_tokenstream_unknown_token)
TEST(test_tokenstream_void_function_signature)
TEST(test_tokenstream_whitespace_ignored)
}; };
int main(int argc, char** argv) { int main(int argc, char** argv) {
/* Declarations first for C89 */
const char** failedTests; const char** failedTests;
int failedCount; int failedCount;
int i;
int j;
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -200,14 +223,16 @@ int main(int argc, char** argv) {
s_totalTests = sizeof(s_tests) / sizeof(s_tests[0]); s_totalTests = sizeof(s_tests) / sizeof(s_tests[0]);
s_greenTests = 0; s_greenTests = 0;
/* Allocate failed tests array dynamically to avoid VLAs */ // Allocate failed tests array dynamically to avoid VLAs
failedTests = (const char**)malloc((s_totalTests + 1) * sizeof(const char*)); failedTests = (const char**)malloc((s_totalTests + 1) * sizeof(const char*));
failedCount = 0; failedCount = 0;
for (i = 0; i < s_totalTests; i++) { for (int i = 0; i < s_totalTests; i++) {
s_currentTestName = s_tests[i].name; // Add 5 to strip the 'test_' prefix.
s_currentTestName = s_tests[i].name + 5;
log_set_output(log_append); log_set_output(log_append);
printf("%s...", s_tests[i].name); printf("%s...", s_tests[i].name);
fflush(stdout);
s_failMsg = NULL; s_failMsg = NULL;
if (setjmp(s_testJmp) == 0) { if (setjmp(s_testJmp) == 0) {
@@ -223,6 +248,16 @@ int main(int argc, char** argv) {
printf(" [FAIL]: %s\n", s_failMsg ? s_failMsg : ""); printf(" [FAIL]: %s\n", s_failMsg ? s_failMsg : "");
failedTests[failedCount++] = s_tests[i].name; failedTests[failedCount++] = s_tests[i].name;
} }
if (s_currentModule) {
parser_free(s_currentModule);
s_currentModule = NULL;
}
if (s_currentTokenStream) {
tokenstream_close(s_currentTokenStream);
s_currentTokenStream = NULL;
}
fflush(stdout);
} }
if (s_testSource) free(s_testSource); if (s_testSource) free(s_testSource);
@@ -230,7 +265,7 @@ int main(int argc, char** argv) {
if (failedCount > 0) { if (failedCount > 0) {
printf("\nFailed tests:\n"); printf("\nFailed tests:\n");
for (j = 0; j < failedCount; j++) { for (int j = 0; j < failedCount; j++) {
printf(" - %s\n", failedTests[j]); printf(" - %s\n", failedTests[j]);
} }
} }
+23 -1
View File
@@ -5,6 +5,7 @@
#define TEST_H #define TEST_H
#include "token.h" #include "token.h"
#include "ast.h"
typedef void (*Test)(void); typedef void (*Test)(void);
@@ -35,6 +36,17 @@ void assert_not_null(void* ptr, const char* msg);
*/ */
void assert_str(const char* expected, const char* actual, const char* msg); void assert_str(const char* expected, const char* actual, const char* msg);
/**
* Asserts that a string has the expected value.
*
* Calls `fail` if the assertion does not hold.
*
* @param expected The expected value. This is typically a string literal.
* @param actual The actual value. This is typically an expression.
* @param msg The message to print if these do not match.
*/
void assert_string(const char* expected, String actual, const char* msg);
/** /**
* Asserts that the logged output matches the expected value. * Asserts that the logged output matches the expected value.
*/ */
@@ -66,7 +78,17 @@ void assert_false(bool condition, const char* msg);
/** /**
* Get the token stream used for this test. * Get the token stream used for this test.
* It reads from the `v0/tests/xyz.c2` file, where xyz is the test name. * It reads from the `v0/tests/xyz.c2` file, where xyz is the test name.
*
* At the end of the test, the tokenstream will be freed automatically by the test harness.
*/ */
TokenStream* tokenstream_get_test(void); TokenStream* test_get_tokenstream(void);
/**
* Gets a parsed module for the this test.
* It reads from the `v0/tests/xyz.c2` file, where xyz is the test name.
*
* At the end of the test, the AST will be freed automatically by the test harness.
*/
Module* test_get_ast(void);
#endif #endif
+2 -2
View File
@@ -34,7 +34,7 @@ static void test_log_on_line(void) {
loc.column_start = 12; loc.column_start = 12;
loc.column_end = 13; loc.column_end = 13;
log_on_line(&loc, 13, "unexpected token"); log_on_line(&loc, "unexpected token");
assert_log_file("expected formatted error message"); assert_log_file("expected formatted error message");
} }
@@ -47,6 +47,6 @@ static void test_log_on_line_variadic(void) {
loc.column_start = 12; loc.column_start = 12;
loc.column_end = 13; loc.column_end = 13;
log_on_line(&loc, 13, "unexpected token '%c'", 'x'); log_on_line(&loc, "unexpected token '%c'", 'x');
assert_log_file("expected formatted error message with variadic args"); assert_log_file("expected formatted error message with variadic args");
} }
+50 -41
View File
@@ -1,86 +1,95 @@
#include "test.h" #include "test.h"
#include "parser.h" #include "parser.h"
#include <string.h> #include <string.h>
#include <stdlib.h>
static void test_parser_module_name(void) { static void test_parser_module_name(void) {
TokenStream* ts = tokenstream_open("test.c", "module my_module;"); Module* m = test_get_ast();
Module* m = parser_parse(ts);
assert_not_null(m, "expected module to be parsed"); assert_not_null(m, "expected module to be parsed");
assert_str("my_module", m->name, "expected name 'my_module'"); assert_str("my_module", m->name, "expected name 'my_module'");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_bad_module_name(void) { static void test_parser_bad_module_name(void) {
TokenStream* ts = tokenstream_get_test(); test_get_ast();
Module* m = parser_parse(ts);
assert_log_file("expected error to be logged for bad module name"); assert_log_file("expected error to be logged for bad module name");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_missing_semicolon_module(void) { static void test_parser_missing_semicolon_module(void) {
TokenStream* ts = tokenstream_get_test(); test_get_ast();
Module* m = parser_parse(ts);
assert_log_file("expected error for missing semicolon"); assert_log_file("expected error for missing semicolon");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_missing_semicolon_import(void) { static void test_parser_missing_semicolon_import(void) {
TokenStream* ts = tokenstream_get_test(); test_get_ast();
Module* m = parser_parse(ts);
assert_log_file("expected error for missing semicolon"); assert_log_file("expected error for missing semicolon");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_bad_import_name(void) { static void test_parser_bad_import_name(void) {
TokenStream* ts = tokenstream_get_test(); test_get_ast();
Module* m = parser_parse(ts);
assert_log_file("expected error for bad import name"); assert_log_file("expected error for bad import name");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_imports(void) { static void test_parser_imports(void) {
TokenStream* ts = tokenstream_open("test.c", "module my_module; import other_module;"); Module* m = test_get_ast();
Module* m = parser_parse(ts);
assert_not_null(m, "expected module to be parsed"); assert_not_null(m, "expected module to be parsed");
assert_str("my_module", m->name, "expected name 'my_module'"); assert_str("my_module", m->name, "expected name 'my_module'");
assert_not_null(m->imports, "expected imports to be parsed"); assert_not_null(m->imports, "expected imports to be parsed");
assert_int(1, m->import_count, "expected one import"); assert_int(1, (int)m->import_count, "expected one import");
assert_str("other_module", m->imports[0].module_name, "expected import name 'other_module'"); assert_str("other_module", m->imports[0].module_name, "expected import name 'other_module'");
assert_false(m->imports[0].is_public, "expected import to not be public"); assert_false(m->imports[0].is_public, "expected import to not be public");
parser_free(m);
tokenstream_close(ts);
} }
static void test_parser_public_imports(void) { static void test_parser_public_imports(void) {
TokenStream* ts = tokenstream_open("test.c", "module my_module; import public other_module;"); Module* m = test_get_ast();
Module* m = parser_parse(ts);
assert_not_null(m, "expected module to be parsed"); assert_not_null(m, "expected module to be parsed");
assert_str("my_module", m->name, "expected name 'my_module'"); assert_str("my_module", m->name, "expected name 'my_module'");
assert_not_null(m->imports, "expected imports to be parsed"); assert_not_null(m->imports, "expected imports to be parsed");
assert_int(1, m->import_count, "expected one import"); assert_int(1, (int)m->import_count, "expected one import");
assert_str("other_module", m->imports[0].module_name, "expected import name 'other_module'"); assert_str("other_module", m->imports[0].module_name, "expected import name 'other_module'");
assert_true(m->imports[0].is_public, "expected import to be public"); assert_true(m->imports[0].is_public, "expected import to be public");
}
parser_free(m);
tokenstream_close(ts); static void test_parser_alias_simple(void) {
Module* m = test_get_ast();
assert_not_null(m, "expected module to be parsed");
assert_int(1, (int)m->alias_count, "expected correct number of aliases");
AliasDeclaration alias = m->aliases[0];
assert_str("myalias", alias.name, "expected correct alias name");
assert_int(TYPE_EXPRESSION_BUILTIN, alias.value.tag, "expected correct alias tag");
assert_int(32, alias.value.builtin.bitSize, "expected bitSize 32");
assert_true(alias.value.builtin.isSigned, "expected signed");
}
static void test_parser_alias_array(void) {
Module* m = test_get_ast();
assert_not_null(m, "expected module to be parsed");
assert_int(1, (int)m->alias_count, "expected correct number of aliases");
AliasDeclaration alias = m->aliases[0];
assert_str("myalias", alias.name, "expected correct alias name");
assert_int(TYPE_EXPRESSION_ARRAY, alias.value.tag, "expected correct alias tag");
TypeExpression* valueType = alias.value.array.array;
assert_not_null(valueType, "expected pointer to array type");
assert_int(TYPE_EXPRESSION_BUILTIN, valueType->tag, "expected correct type tag");
assert_int(32, valueType->builtin.bitSize, "expected bitSize 32");
assert_true(valueType->builtin.isSigned, "expected signed");
}
static void test_parser_alias_and_import_mix(void) {
Module* m = test_get_ast();
assert_not_null(m, "expected module to be parsed");
assert_int(2, (int)m->import_count, "expected 2 imports");
assert_int(2, (int)m->alias_count, "expected 2 aliases");
assert_str("foo", m->imports[0].module_name, "expected import 1 name 'foo'");
assert_str("bar", m->imports[1].module_name, "expected import 2 name 'bar'");
assert_str("myalias", m->aliases[0].name, "expected alias 1 name 'myalias'");
assert_str("otheralias", m->aliases[1].name, "expected alias 2 name 'otheralias'");
} }
+10 -44
View File
@@ -9,23 +9,19 @@ static void test_tokenstream_open_fail(void) {
} }
static void test_tokenstream_simple_keyword(void) { static void test_tokenstream_simple_keyword(void) {
TokenStream* ts; TokenStream* ts = test_get_tokenstream();
Token t; Token t;
Token eof; Token eof;
ts = tokenstream_open("test.c", "module");
t = tokenstream_next(ts); t = tokenstream_next(ts);
if (t.token != TOKEN_MODULE) fail("expected TOKEN_MODULE"); if (t.token != TOKEN_MODULE) fail("expected TOKEN_MODULE");
eof = tokenstream_next(ts); eof = tokenstream_next(ts);
if (eof.token != TOKEN_EOF) fail("expected EOF"); if (eof.token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_keywords_and_symbols(void) { static void test_tokenstream_keywords_and_symbols(void) {
TokenStream* ts = tokenstream_open("test.c", "module main; import stdio;"); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_MODULE) fail("expected TOKEN_MODULE"); if (tokenstream_next(ts).token != TOKEN_MODULE) fail("expected TOKEN_MODULE");
if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER (main)"); if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER (main)");
@@ -34,24 +30,20 @@ static void test_tokenstream_keywords_and_symbols(void) {
if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER (stdio)"); if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER (stdio)");
if (tokenstream_next(ts).token != TOKEN_SEMICOLON) fail("expected TOKEN_SEMICOLON"); if (tokenstream_next(ts).token != TOKEN_SEMICOLON) fail("expected TOKEN_SEMICOLON");
if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF"); if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_parentheses_and_brackets(void) { static void test_tokenstream_parentheses_and_brackets(void) {
TokenStream* ts = tokenstream_open("test.c", "()[]"); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_PARENT_OPEN) fail("expected TOKEN_PARENT_OPEN"); if (tokenstream_next(ts).token != TOKEN_PARENT_OPEN) fail("expected TOKEN_PARENT_OPEN");
if (tokenstream_next(ts).token != TOKEN_PARENT_CLOSE) fail("expected TOKEN_PARENT_CLOSE"); if (tokenstream_next(ts).token != TOKEN_PARENT_CLOSE) fail("expected TOKEN_PARENT_CLOSE");
if (tokenstream_next(ts).token != TOKEN_BRACKET_OPEN) fail("expected TOKEN_BRACKET_OPEN"); if (tokenstream_next(ts).token != TOKEN_BRACKET_OPEN) fail("expected TOKEN_BRACKET_OPEN");
if (tokenstream_next(ts).token != TOKEN_BRACKET_CLOSE) fail("expected TOKEN_BRACKET_CLOSE"); if (tokenstream_next(ts).token != TOKEN_BRACKET_CLOSE) fail("expected TOKEN_BRACKET_CLOSE");
if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF"); if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_comma(void) { static void test_tokenstream_comma(void) {
TokenStream* ts = tokenstream_open("test.c", "a,b,c"); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected a"); if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected a");
if (tokenstream_next(ts).token != TOKEN_COMMA) fail("expected comma"); if (tokenstream_next(ts).token != TOKEN_COMMA) fail("expected comma");
@@ -59,75 +51,49 @@ static void test_tokenstream_comma(void) {
if (tokenstream_next(ts).token != TOKEN_COMMA) fail("expected comma"); if (tokenstream_next(ts).token != TOKEN_COMMA) fail("expected comma");
if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected c"); if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected c");
if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF"); if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_whitespace_ignored(void) { static void test_tokenstream_whitespace_ignored(void) {
TokenStream* ts = tokenstream_open("test.c", " module \n\t import ; "); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_MODULE) fail("expected TOKEN_MODULE"); if (tokenstream_next(ts).token != TOKEN_MODULE) fail("expected TOKEN_MODULE");
if (tokenstream_next(ts).token != TOKEN_IMPORT) fail("expected TOKEN_IMPORT"); if (tokenstream_next(ts).token != TOKEN_IMPORT) fail("expected TOKEN_IMPORT");
if (tokenstream_next(ts).token != TOKEN_SEMICOLON) fail("expected TOKEN_SEMICOLON"); if (tokenstream_next(ts).token != TOKEN_SEMICOLON) fail("expected TOKEN_SEMICOLON");
if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF"); if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_void_function_signature(void) { static void test_tokenstream_void_function_signature(void) {
TokenStream* ts = tokenstream_open("test.c", "void main()"); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_VOID) fail("expected TOKEN_VOID"); if (tokenstream_next(ts).token != TOKEN_VOID) fail("expected TOKEN_VOID");
if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER"); if (tokenstream_next(ts).token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER");
if (tokenstream_next(ts).token != TOKEN_PARENT_OPEN) fail("expected TOKEN_PARENT_OPEN"); if (tokenstream_next(ts).token != TOKEN_PARENT_OPEN) fail("expected TOKEN_PARENT_OPEN");
if (tokenstream_next(ts).token != TOKEN_PARENT_CLOSE) fail("expected TOKEN_PARENT_CLOSE"); if (tokenstream_next(ts).token != TOKEN_PARENT_CLOSE) fail("expected TOKEN_PARENT_CLOSE");
if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF"); if (tokenstream_next(ts).token != TOKEN_EOF) fail("expected EOF");
tokenstream_close(ts);
} }
static void test_tokenstream_unknown_token(void) { static void test_tokenstream_unknown_token(void) {
TokenStream* ts = tokenstream_get_test(); TokenStream* ts = test_get_tokenstream();
if (tokenstream_next(ts).token != TOKEN_UNKNOWN) fail("expected TOKEN_UNKNOWN"); if (tokenstream_next(ts).token != TOKEN_UNKNOWN) fail("expected TOKEN_UNKNOWN");
assert_log_file("expected error message for unknown token"); assert_log_file("expected error message for unknown token");
tokenstream_close(ts);
} }
static void test_tokenstream_info(void) { static void test_tokenstream_info(void) {
TokenStream* ts; TokenStream* ts = test_get_tokenstream();
Token t1; Token t1;
Token t2; Token t2;
char* buf1;
char* buf2;
ts = tokenstream_open("test.c", "module main;");
t1 = tokenstream_next(ts); t1 = tokenstream_next(ts);
if (t1.token != TOKEN_MODULE) fail("expected TOKEN_MODULE"); if (t1.token != TOKEN_MODULE) fail("expected TOKEN_MODULE");
assert_string("module", t1.text, "info: expected 'module'");
buf1 = malloc((size_t)t1.text.length + 1);
if (!buf1) fail("out of memory");
memcpy(buf1, t1.text.data, t1.text.length);
buf1[t1.text.length] = '\0';
assert_str("module", buf1, "info: expected 'module'");
if (t1.location.line != 1) fail("expected line 1"); if (t1.location.line != 1) fail("expected line 1");
if (t1.location.column_start != 1) fail("expected column 1"); if (t1.location.column_start != 1) fail("expected column 1");
t2 = tokenstream_next(ts); t2 = tokenstream_next(ts);
if (t2.token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER"); if (t2.token != TOKEN_IDENTIFIER) fail("expected TOKEN_IDENTIFIER");
assert_string("main", t2.text, "info: expected 'main'");
buf2 = malloc((size_t)t2.text.length + 1);
if (!buf2) { free(buf1); fail("out of memory"); }
memcpy(buf2, t2.text.data, t2.text.length);
buf2[t2.text.length] = '\0';
assert_str("main", buf2, "info: expected 'main'");
if (t2.location.line != 1) fail("expected line 1"); if (t2.location.line != 1) fail("expected line 1");
if (t2.location.column_start != 8) fail("expected column 8"); if (t2.location.column_start != 8) fail("expected column 8");
free(buf1);
free(buf2);
tokenstream_close(ts);
} }
+9
View File
@@ -0,0 +1,9 @@
module mymodule;
import foo;
alias myalias = int32[];
import bar;
alias otheralias = int32;
+3
View File
@@ -0,0 +1,3 @@
module mymodule;
alias myalias = int32[];
+3
View File
@@ -0,0 +1,3 @@
module mymodule;
alias myalias = int32;
+2
View File
@@ -0,0 +1,2 @@
module my_module;
import other_module;
+1
View File
@@ -0,0 +1 @@
module my_module;
+3
View File
@@ -0,0 +1,3 @@
module my_module;
public import other_module;
+1
View File
@@ -0,0 +1 @@
a,b,c
+1
View File
@@ -0,0 +1 @@
module main;
@@ -0,0 +1 @@
module main; import stdio;
@@ -0,0 +1 @@
()[]
+1
View File
@@ -0,0 +1 @@
module
@@ -0,0 +1 @@
void main()
@@ -0,0 +1,2 @@
module
import ;
+4 -2
View File
@@ -26,10 +26,11 @@ typedef struct {
const char* keyword; const char* keyword;
TokenType token; TokenType token;
} KeywordMap; } KeywordMap;
static const KeywordMap keywords[] = { static const KeywordMap keywords[] = {
{"module", TOKEN_MODULE}, {"module", TOKEN_MODULE},
{"import", TOKEN_IMPORT}, {"import", TOKEN_IMPORT},
{"alias", TOKEN_ALIAS},
{"public", TOKEN_PUBLIC},
{"void", TOKEN_VOID}, {"void", TOKEN_VOID},
}; };
@@ -220,6 +221,7 @@ Token tokenstream_next(TokenStream* ts) {
case ']': return create_token(ts, TOKEN_BRACKET_CLOSE, start_text, 1, start_line, start_column, line_start); case ']': return create_token(ts, TOKEN_BRACKET_CLOSE, start_text, 1, start_line, start_column, line_start);
case ',': return create_token(ts, TOKEN_COMMA, start_text, 1, start_line, start_column, line_start); case ',': return create_token(ts, TOKEN_COMMA, start_text, 1, start_line, start_column, line_start);
case ';': return create_token(ts, TOKEN_SEMICOLON, start_text, 1, start_line, start_column, line_start); case ';': return create_token(ts, TOKEN_SEMICOLON, start_text, 1, start_line, start_column, line_start);
case '=': return create_token(ts, TOKEN_ASSIGN, start_text, 1, start_line, start_column, line_start);
} }
/* Keywords and identifiers */ /* Keywords and identifiers */
@@ -239,6 +241,6 @@ Token tokenstream_next(TokenStream* ts) {
/* Unknown character */ /* Unknown character */
t = create_token(ts, TOKEN_UNKNOWN, start_text, 1, start_line, start_column, line_start); t = create_token(ts, TOKEN_UNKNOWN, start_text, 1, start_line, start_column, line_start);
log_on_line(&t.location, t.location.column_end, "unexpected token '%c'", c); log_on_line(&t.location, "unexpected token '%c'", c);
return t; return t;
} }
+4
View File
@@ -14,6 +14,8 @@ typedef enum {
TOKEN_MODULE, TOKEN_MODULE,
TOKEN_IMPORT, TOKEN_IMPORT,
TOKEN_SEMICOLON, TOKEN_SEMICOLON,
TOKEN_ALIAS,
TOKEN_PUBLIC,
/* Symbols */ /* Symbols */
TOKEN_PARENT_OPEN, TOKEN_PARENT_OPEN,
@@ -21,9 +23,11 @@ typedef enum {
TOKEN_BRACKET_OPEN, TOKEN_BRACKET_OPEN,
TOKEN_BRACKET_CLOSE, TOKEN_BRACKET_CLOSE,
TOKEN_COMMA, TOKEN_COMMA,
TOKEN_ASSIGN,
/* Primitives */ /* Primitives */
TOKEN_VOID, TOKEN_VOID,
TOKEN_BUILTIN_TYPE,
/* Variable */ /* Variable */
TOKEN_IDENTIFIER, TOKEN_IDENTIFIER,
+9
View File
@@ -0,0 +1,9 @@
/**
* Contains runtime information about types.
*/
#ifndef TYPES_H
#define TYPES_H
#endif