diff --git a/ezc/Makefile b/ezc/Makefile index 5f0d21c..66f2442 100644 --- a/ezc/Makefile +++ b/ezc/Makefile @@ -47,7 +47,7 @@ $(BIN): $(OBJ) $(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $< clean: - rm -f $(OBJ) $(BIN) tests/test_lexer tests/test_parser + rm -f $(OBJ) $(BIN) tests/test_lexer tests/test_parser tests/test_typechecker tests/test_codegen install: build install -d /usr/local/bin @@ -73,14 +73,27 @@ tests/test_lexer: tests/test_lexer.c $(TEST_DEPS) tests/test_parser: tests/test_parser.c $(TEST_DEPS) $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) -test-unit: $(TEST_DEPS) tests/test_lexer tests/test_parser +tests/test_typechecker: tests/test_typechecker.c $(TEST_DEPS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) + +tests/test_codegen: tests/test_codegen.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $< $(LDFLAGS) + +test-unit: $(TEST_DEPS) tests/test_lexer tests/test_parser tests/test_typechecker @echo "" @echo "=== Lexer Tests ===" @./tests/test_lexer @echo "=== Parser Tests ===" @./tests/test_parser + @echo "=== Type Checker Tests ===" + @./tests/test_typechecker + +test-e2e: build tests/test_codegen + @echo "" + @echo "=== End-to-End Codegen Tests ===" + @./tests/test_codegen test-parity: build @./tests/run_parity.sh -test: test-unit test-parity +test: test-unit test-e2e test-parity diff --git a/ezc/src/parser/parser.c b/ezc/src/parser/parser.c index 70bce47..96f06d2 100644 --- a/ezc/src/parser/parser.c +++ b/ezc/src/parser/parser.c @@ -683,15 +683,34 @@ static AstNode *parse_func_declaration(Parser *p) { * -> (int, string) plain types * -> (x int, y int) named returns * -> (x, y int) shared type + * + * Disambiguation: if the identifier is a known type name, + * it's a plain type list, not names. */ next_token(p); while (!cur_token_is(p, TOK_RPAREN) && !cur_token_is(p, TOK_EOF)) { - if (cur_token_is(p, TOK_IDENT) && peek_token_is(p, TOK_IDENT)) { + /* Check if current ident is a type name (not a variable name) */ + bool is_type = false; + if (cur_token_is(p, TOK_IDENT)) { + const char *lit = p->cur_token.literal; + is_type = (strcmp(lit, "int") == 0 || strcmp(lit, "uint") == 0 || + strcmp(lit, "i8") == 0 || strcmp(lit, "i16") == 0 || + strcmp(lit, "i32") == 0 || strcmp(lit, "i64") == 0 || + strcmp(lit, "u8") == 0 || strcmp(lit, "u16") == 0 || + strcmp(lit, "u32") == 0 || strcmp(lit, "u64") == 0 || + strcmp(lit, "float") == 0 || strcmp(lit, "f32") == 0 || + strcmp(lit, "f64") == 0 || strcmp(lit, "string") == 0 || + strcmp(lit, "bool") == 0 || strcmp(lit, "char") == 0 || + strcmp(lit, "byte") == 0 || + (lit[0] >= 'A' && lit[0] <= 'Z')); /* struct/enum types */ + } + + if (cur_token_is(p, TOK_IDENT) && peek_token_is(p, TOK_IDENT) && !is_type) { /* Named return: name type — skip name, use type */ next_token(p); node->data.func_decl.return_types[node->data.func_decl.return_type_count++] = p->cur_token.literal; - } else if (cur_token_is(p, TOK_IDENT) && peek_token_is(p, TOK_COMMA)) { + } else if (cur_token_is(p, TOK_IDENT) && peek_token_is(p, TOK_COMMA) && !is_type) { /* Shared type: (x, y int) — count names, assign same type to all */ int shared = 1; while (peek_token_is(p, TOK_COMMA)) { diff --git a/ezc/tests/test_codegen.c b/ezc/tests/test_codegen.c new file mode 100644 index 0000000..367bca3 --- /dev/null +++ b/ezc/tests/test_codegen.c @@ -0,0 +1,454 @@ +/* + * test_codegen.c - End-to-end tests for EZC code generation + * + * Each test compiles an EZ snippet, runs the resulting binary, + * and checks the output. + * + * Copyright (c) 2025-Present Marshall A Burns + * Licensed under the MIT License. See LICENSE for details. + */ + +#include "test.h" +#include +#include +#include +#include + +static int test_num = 0; + +/* Compile and run an EZ program, return its stdout output */ +static char *compile_and_run(const char *ez_source) { + test_num++; + static char output[4096]; + char ez_file[128], bin_file[128]; + + snprintf(ez_file, sizeof(ez_file), "/tmp/ezc_e2e_%d.ez", test_num); + snprintf(bin_file, sizeof(bin_file), "/tmp/ezc_e2e_%d", test_num); + + /* Write source */ + FILE *f = fopen(ez_file, "w"); + if (!f) return NULL; + fputs(ez_source, f); + fclose(f); + + /* Compile */ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "./ezc %s -o %s 2>/dev/null", ez_file, bin_file); + if (system(cmd) != 0) { + unlink(ez_file); + return NULL; + } + + /* Run and capture output */ + char run_cmd[256]; + snprintf(run_cmd, sizeof(run_cmd), "%s 2>&1", bin_file); + FILE *p = popen(run_cmd, "r"); + if (!p) { + unlink(ez_file); + unlink(bin_file); + return NULL; + } + + size_t total = 0; + size_t n; + while ((n = fread(output + total, 1, sizeof(output) - total - 1, p)) > 0) { + total += n; + } + output[total] = '\0'; + pclose(p); + + /* Remove trailing newline for easier comparison */ + if (total > 0 && output[total - 1] == '\n') { + output[total - 1] = '\0'; + } + + unlink(ez_file); + unlink(bin_file); + return output; +} + +/* --- Hello World --- */ + +static void test_e2e_hello(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() { println(\"Hello, World!\") }"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "Hello, World!"); +} + +/* --- Arithmetic --- */ + +static void test_e2e_arithmetic(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " println(2 + 3)\n" + " println(10 - 4)\n" + " println(3 * 7)\n" + " println(20 / 4)\n" + " println(17 % 5)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "5\n6\n21\n5\n2"); +} + +/* --- Variables --- */ + +static void test_e2e_variables(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp x int = 10\n" + " temp y int = 20\n" + " println(x + y)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "30"); +} + +/* --- String interpolation --- */ + +static void test_e2e_interpolation(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp name string = \"Alice\"\n" + " temp age int = 30\n" + " println(\"${name} is ${age}\")\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "Alice is 30"); +} + +/* --- If/or/otherwise --- */ + +static void test_e2e_if_else(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp x int = 5\n" + " if x > 10 {\n" + " println(\"big\")\n" + " } or x > 3 {\n" + " println(\"medium\")\n" + " } otherwise {\n" + " println(\"small\")\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "medium"); +} + +/* --- For loop --- */ + +static void test_e2e_for_range(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " for i in range(1, 4) {\n" + " println(i)\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "1\n2\n3"); +} + +/* --- While loop --- */ + +static void test_e2e_while(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp i int = 0\n" + " as_long_as i < 3 {\n" + " println(i)\n" + " i++\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "0\n1\n2"); +} + +/* --- Loop with break --- */ + +static void test_e2e_loop_break(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp i int = 0\n" + " loop {\n" + " if i >= 3 { break }\n" + " println(i)\n" + " i++\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "0\n1\n2"); +} + +/* --- Functions --- */ + +static void test_e2e_function_call(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do add(a int, b int) -> int { return a + b }\n" + "do main() { println(add(3, 4)) }"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "7"); +} + +/* --- Recursion --- */ + +static void test_e2e_recursion(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do fib(n int) -> int {\n" + " if n <= 1 { return n }\n" + " return fib(n - 1) + fib(n - 2)\n" + "}\n" + "do main() { println(fib(10)) }"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "55"); +} + +/* --- Multiple returns --- */ + +static void test_e2e_multi_return(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do swap(a int, b int) -> (int, int) { return b, a }\n" + "do main() {\n" + " temp x int, y int = swap(10, 20)\n" + " println(x)\n" + " println(y)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "20\n10"); +} + +/* --- Mutable params --- */ + +static void test_e2e_mutable_param(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do inc(&n int) { n = n + 1 }\n" + "do main() {\n" + " temp x int = 5\n" + " inc(x)\n" + " println(x)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "6"); +} + +/* --- Ensure --- */ + +static void test_e2e_ensure(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do cleanup() { println(\"cleaned\") }\n" + "do work() {\n" + " ensure cleanup()\n" + " println(\"working\")\n" + "}\n" + "do main() { work() }"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "working\ncleaned"); +} + +/* --- Structs --- */ + +static void test_e2e_struct(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "const Point struct {\n" + " x int\n" + " y int\n" + "}\n" + "do main() {\n" + " temp p Point = Point{x: 3, y: 4}\n" + " println(p.x)\n" + " println(p.y)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "3\n4"); +} + +/* --- Enums --- */ + +static void test_e2e_enum(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "const Color enum { RED\n GREEN\n BLUE }\n" + "do main() {\n" + " temp c Color = Color.GREEN\n" + " println(c)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "1"); +} + +/* --- Arrays --- */ + +static void test_e2e_array(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp nums [int] = {10, 20, 30}\n" + " println(nums[0])\n" + " println(nums[2])\n" + " println(len(nums))\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "10\n30\n3"); +} + +/* --- Array mutation --- */ + +static void test_e2e_array_set(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp nums [int] = {1, 2, 3}\n" + " nums[1] = 99\n" + " println(nums[1])\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "99"); +} + +/* --- For each --- */ + +static void test_e2e_for_each(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp names [string] = {\"a\", \"b\", \"c\"}\n" + " for_each name in names {\n" + " println(name)\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "a\nb\nc"); +} + +/* --- When/Is --- */ + +static void test_e2e_when(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp x int = 2\n" + " when x {\n" + " is 1 { println(\"one\") }\n" + " is 2 { println(\"two\") }\n" + " default { println(\"other\") }\n" + " }\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "two"); +} + +/* --- Len builtin --- */ + +static void test_e2e_len_string(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() { println(len(\"hello\")) }"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "5"); +} + +/* --- Typeof builtin --- */ + +static void test_e2e_typeof(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " println(typeof(42))\n" + " println(typeof(\"hi\"))\n" + " println(typeof(true))\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "int\nstring\nbool"); +} + +/* --- Compound assignment --- */ + +static void test_e2e_compound_assign(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp x int = 10\n" + " x += 5\n" + " x -= 3\n" + " x *= 2\n" + " println(x)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "24"); +} + +/* --- Char type --- */ + +static void test_e2e_char(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do main() {\n" + " temp c char = 'A'\n" + " println(c)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "65"); +} + +/* --- Blank identifier --- */ + +static void test_e2e_blank_ident(void) { + char *out = compile_and_run( + "import @std\nusing std\n" + "do pair() -> (int, int) { return 42, 99 }\n" + "do main() {\n" + " temp _, b int = pair()\n" + " println(b)\n" + "}"); + ASSERT_NOT_NULL(out); + ASSERT_STR_EQ(out, "99"); +} + +int main(void) { + /* Must run from the ezc/ directory */ + if (access("./ezc", X_OK) != 0) { + fprintf(stderr, "test_codegen: must run from the ezc/ directory\n"); + return 1; + } + + printf("\n"); + RUN_TEST(test_e2e_hello); + RUN_TEST(test_e2e_arithmetic); + RUN_TEST(test_e2e_variables); + RUN_TEST(test_e2e_interpolation); + RUN_TEST(test_e2e_if_else); + RUN_TEST(test_e2e_for_range); + RUN_TEST(test_e2e_while); + RUN_TEST(test_e2e_loop_break); + RUN_TEST(test_e2e_function_call); + RUN_TEST(test_e2e_recursion); + RUN_TEST(test_e2e_multi_return); + RUN_TEST(test_e2e_mutable_param); + RUN_TEST(test_e2e_ensure); + RUN_TEST(test_e2e_struct); + RUN_TEST(test_e2e_enum); + RUN_TEST(test_e2e_array); + RUN_TEST(test_e2e_array_set); + RUN_TEST(test_e2e_for_each); + RUN_TEST(test_e2e_when); + RUN_TEST(test_e2e_len_string); + RUN_TEST(test_e2e_typeof); + RUN_TEST(test_e2e_compound_assign); + RUN_TEST(test_e2e_char); + RUN_TEST(test_e2e_blank_ident); + PRINT_RESULTS(); + return _test_fail > 0 ? 1 : 0; +} diff --git a/ezc/tests/test_typechecker.c b/ezc/tests/test_typechecker.c new file mode 100644 index 0000000..a978603 --- /dev/null +++ b/ezc/tests/test_typechecker.c @@ -0,0 +1,320 @@ +/* + * test_typechecker.c - Unit tests for the EZC type checker + * + * Copyright (c) 2025-Present Marshall A Burns + * Licensed under the MIT License. See LICENSE for details. + */ + +#include "test.h" +#include "../src/util/arena.h" +#include "../src/util/error.h" +#include "../src/lexer/lexer.h" +#include "../src/parser/parser.h" +#include "../src/typechecker/typechecker.h" + +static Arena *arena; + +static TypeTable *check(const char *input) { + DiagnosticList *diag = diag_create(); + diag->use_color = false; + Lexer *l = lexer_create(arena, input, "test.ez"); + Parser *p = parser_create(arena, l, "test.ez", diag); + AstNode *prog = parser_parse_program(p); + TypeChecker *tc = typechecker_create(diag, "test.ez"); + typechecker_check(tc, prog); + return typechecker_get_table(tc); +} + +/* Helper: parse, type check, and get the type of the first expression in main */ +static EzType *expr_type(const char *expr_code) { + char buf[512]; + snprintf(buf, sizeof(buf), + "do main() { temp _result = %s }", expr_code); + DiagnosticList *diag = diag_create(); + diag->use_color = false; + Lexer *l = lexer_create(arena, buf, "test.ez"); + Parser *p = parser_create(arena, l, "test.ez", diag); + AstNode *prog = parser_parse_program(p); + TypeChecker *tc = typechecker_create(diag, "test.ez"); + typechecker_check(tc, prog); + TypeTable *tt = typechecker_get_table(tc); + + /* Find the var decl's value and look up its type */ + AstNode *main_fn = prog->data.program.stmts[0]; + AstNode *var_decl = main_fn->data.func_decl.body->data.block.stmts[0]; + if (var_decl->kind == NODE_VAR_DECL && var_decl->data.var_decl.value) { + return typetable_get(tt, var_decl->data.var_decl.value); + } + return NULL; +} + +/* --- Scope and Symbol Table --- */ + +static void test_scope_define_lookup(void) { + Scope *s = scope_create(NULL); + scope_define(s, "x", &TYPE_INT, true); + Symbol *sym = scope_lookup(s, "x"); + ASSERT_NOT_NULL(sym); + ASSERT_EQ(sym->type->kind, TK_INT); + ASSERT(sym->mutable); +} + +static void test_scope_nested(void) { + Scope *outer = scope_create(NULL); + scope_define(outer, "x", &TYPE_INT, true); + Scope *inner = scope_create(outer); + scope_define(inner, "y", &TYPE_STRING, false); + + /* inner can see both */ + ASSERT_NOT_NULL(scope_lookup(inner, "x")); + ASSERT_NOT_NULL(scope_lookup(inner, "y")); + + /* outer can only see x */ + ASSERT_NOT_NULL(scope_lookup(outer, "x")); + ASSERT(scope_lookup(outer, "y") == NULL); +} + +static void test_scope_shadow(void) { + Scope *outer = scope_create(NULL); + scope_define(outer, "x", &TYPE_INT, true); + Scope *inner = scope_create(outer); + scope_define(inner, "x", &TYPE_STRING, false); + + /* inner sees the shadowed string version */ + Symbol *sym = scope_lookup_local(inner, "x"); + ASSERT_NOT_NULL(sym); + ASSERT_EQ(sym->type->kind, TK_STRING); +} + +static void test_scope_undefined(void) { + Scope *s = scope_create(NULL); + ASSERT(scope_lookup(s, "nonexistent") == NULL); +} + +/* --- Type Constructors --- */ + +static void test_type_from_name_primitives(void) { + ASSERT_EQ(type_from_name("int")->kind, TK_INT); + ASSERT_EQ(type_from_name("i8")->kind, TK_INT); + ASSERT_EQ(type_from_name("i16")->kind, TK_INT); + ASSERT_EQ(type_from_name("i32")->kind, TK_INT); + ASSERT_EQ(type_from_name("i64")->kind, TK_INT); + ASSERT_EQ(type_from_name("u8")->kind, TK_INT); + ASSERT_EQ(type_from_name("u16")->kind, TK_INT); + ASSERT_EQ(type_from_name("u32")->kind, TK_INT); + ASSERT_EQ(type_from_name("u64")->kind, TK_INT); + ASSERT_EQ(type_from_name("uint")->kind, TK_INT); + ASSERT_EQ(type_from_name("float")->kind, TK_FLOAT); + ASSERT_EQ(type_from_name("f32")->kind, TK_FLOAT); + ASSERT_EQ(type_from_name("f64")->kind, TK_FLOAT); + ASSERT_EQ(type_from_name("bool")->kind, TK_BOOL); + ASSERT_EQ(type_from_name("char")->kind, TK_CHAR); + ASSERT_EQ(type_from_name("byte")->kind, TK_BYTE); + ASSERT_EQ(type_from_name("string")->kind, TK_STRING); + ASSERT_EQ(type_from_name("void")->kind, TK_VOID); + ASSERT_EQ(type_from_name("nil")->kind, TK_NIL); +} + +static void test_type_from_name_array(void) { + EzType *t = type_from_name("[int]"); + ASSERT_EQ(t->kind, TK_ARRAY); + ASSERT_STR_EQ(t->element_type, "int"); +} + +static void test_type_from_name_struct(void) { + EzType *t = type_from_name("Person"); + ASSERT_EQ(t->kind, TK_STRUCT); + ASSERT_STR_EQ(t->name, "Person"); +} + +static void test_type_is_numeric(void) { + ASSERT(type_is_numeric(&TYPE_INT)); + ASSERT(type_is_numeric(&TYPE_FLOAT)); + ASSERT(type_is_numeric(&TYPE_CHAR)); + ASSERT(type_is_numeric(&TYPE_BYTE)); + ASSERT(!type_is_numeric(&TYPE_STRING)); + ASSERT(!type_is_numeric(&TYPE_BOOL)); +} + +/* --- Expression Type Resolution --- */ + +static void test_resolve_int_literal(void) { + EzType *t = expr_type("42"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_INT); +} + +static void test_resolve_float_literal(void) { + EzType *t = expr_type("3.14"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_FLOAT); +} + +static void test_resolve_string_literal(void) { + EzType *t = expr_type("\"hello\""); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_STRING); +} + +static void test_resolve_bool_literal(void) { + EzType *t = expr_type("true"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_BOOL); +} + +static void test_resolve_nil(void) { + EzType *t = expr_type("nil"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_NIL); +} + +static void test_resolve_arithmetic(void) { + EzType *t = expr_type("1 + 2"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_INT); +} + +static void test_resolve_float_arithmetic(void) { + EzType *t = expr_type("1.0 + 2"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_FLOAT); +} + +static void test_resolve_comparison(void) { + EzType *t = expr_type("1 < 2"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_BOOL); +} + +static void test_resolve_logical(void) { + EzType *t = expr_type("true && false"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_BOOL); +} + +static void test_resolve_negation(void) { + EzType *t = expr_type("-42"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_INT); +} + +static void test_resolve_not(void) { + EzType *t = expr_type("!true"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_BOOL); +} + +static void test_resolve_array_literal(void) { + EzType *t = expr_type("{1, 2, 3}"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_ARRAY); +} + +/* --- Variable Type Resolution --- */ + +static void test_resolve_typed_var(void) { + TypeTable *tt = check( + "do main() {\n" + " temp x int = 42\n" + " temp y string = \"hello\"\n" + "}"); + (void)tt; + /* If it doesn't crash, the scope correctly handled the declarations */ + ASSERT_NOT_NULL(tt); +} + +/* --- Builtin Function Types --- */ + +static void test_resolve_len(void) { + EzType *t = expr_type("len(\"hello\")"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_INT); +} + +static void test_resolve_typeof(void) { + EzType *t = expr_type("typeof(42)"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_STRING); +} + +static void test_resolve_to_float(void) { + EzType *t = expr_type("to_float(42)"); + ASSERT_NOT_NULL(t); + ASSERT_EQ(t->kind, TK_FLOAT); +} + +/* --- Struct Type Resolution --- */ + +static void test_resolve_struct_field(void) { + TypeTable *tt = check( + "const Person struct {\n" + " name string\n" + " age int\n" + "}\n" + "do main() {\n" + " temp p Person = Person{name: \"Alice\", age: 30}\n" + " temp n = p.name\n" + "}"); + (void)tt; + ASSERT_NOT_NULL(tt); +} + +/* --- Function Return Type Resolution --- */ + +static void test_resolve_func_return(void) { + TypeTable *tt = check( + "do add(a int, b int) -> int { return a + b }\n" + "do main() {\n" + " temp result = add(1, 2)\n" + "}"); + (void)tt; + ASSERT_NOT_NULL(tt); +} + +int main(void) { + arena = arena_create(256 * 1024); + printf("\n"); + + /* Scope tests */ + RUN_TEST(test_scope_define_lookup); + RUN_TEST(test_scope_nested); + RUN_TEST(test_scope_shadow); + RUN_TEST(test_scope_undefined); + + /* Type constructor tests */ + RUN_TEST(test_type_from_name_primitives); + RUN_TEST(test_type_from_name_array); + RUN_TEST(test_type_from_name_struct); + RUN_TEST(test_type_is_numeric); + + /* Expression resolution tests */ + RUN_TEST(test_resolve_int_literal); + RUN_TEST(test_resolve_float_literal); + RUN_TEST(test_resolve_string_literal); + RUN_TEST(test_resolve_bool_literal); + RUN_TEST(test_resolve_nil); + RUN_TEST(test_resolve_arithmetic); + RUN_TEST(test_resolve_float_arithmetic); + RUN_TEST(test_resolve_comparison); + RUN_TEST(test_resolve_logical); + RUN_TEST(test_resolve_negation); + RUN_TEST(test_resolve_not); + RUN_TEST(test_resolve_array_literal); + + /* Variable tests */ + RUN_TEST(test_resolve_typed_var); + + /* Builtin tests */ + RUN_TEST(test_resolve_len); + RUN_TEST(test_resolve_typeof); + RUN_TEST(test_resolve_to_float); + + /* Struct tests */ + RUN_TEST(test_resolve_struct_field); + + /* Function tests */ + RUN_TEST(test_resolve_func_return); + + PRINT_RESULTS(); + return _test_fail > 0 ? 1 : 0; +}