diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..da8fb78 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "try building projects" + +make clean +make + +if [ $? -ne 0 ]; then + echo "project compilation fail, please fix this" + exit 1 +fi + +echo "running tests" + +make test + +if [ $? -ne 0 ]; then + echo "tests fails" + echo "either fix them or commit using 'git commit --no-verify' if failing is intended" + exit 1 +fi + +echo "running valgring tests" + +make valgrind-test + +if [ $? -ne 0 ]; then + echo "tests fails" + echo "either fix them or commit using 'git commit --no-verify' if failing is intended" + exit 1 +fi + +make integration-test + +if [ $? -ne 0 ];then + echo "integration tests fails" + echo "either fix them or commit using 'git commit --no-verify' if failing is intended" + exit 1 +fi + +echo "all tests passed" +exit 0 diff --git a/.github/workflows/asan.yml b/.github/workflows/asan.yml index 2dc7945..6a7c9b6 100644 --- a/.github/workflows/asan.yml +++ b/.github/workflows/asan.yml @@ -13,6 +13,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y nasm binutils + - name: Build & test with ASan/UBSan run: | CFLAGS="-fsanitize=address,undefined -g -O1" make asan-test || make test diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..7692db6 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,24 @@ +name: C Integration Tests + +on: + push: + branches: + - "**" + pull_request: + branches: + - main + +jobs: + test: + name: Run integration tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y nasm binutils + + - name: integration test + run: | + make integration-test diff --git a/.gitignore b/.gitignore index e85f81d..1009129 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +!src/compiler/build *.swp *.log a.out diff --git a/Makefile b/Makefile index 1a0b133..af45cdb 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,12 @@ CS = \ $(SRC)/backend/x86_64.c \ $(SRC)/backend/codegen.c \ $(SRC)/compiler/definition/compiler_definition.c \ + $(SRC)/compiler/setup/compiler_setup.c \ + $(SRC)/compiler/build/file_scanner.c \ + $(SRC)/compiler/build/registry.c \ + $(SRC)/compiler/build/dep_graph.c \ + $(SRC)/compiler/build/export_table.c \ + $(SRC)/compiler/build/import_resolver.c \ OBJ = \ $(BUILD)/cleaf.o \ @@ -23,19 +29,24 @@ OBJ = \ $(BUILD)/backend/x86_64.o \ $(BUILD)/backend/codegen.o \ $(BUILD)/compiler/definition/compiler_definition.o \ + $(BUILD)/compiler/setup/compiler_setup.o \ + $(BUILD)/compiler/build/file_scanner.o \ + $(BUILD)/compiler/build/registry.o \ + $(BUILD)/compiler/build/dep_graph.o \ + $(BUILD)/compiler/build/export_table.o \ + $(BUILD)/compiler/build/import_resolver.o \ CC = gcc CFLAGS = -Wall -Wextra -g -Isrc VALGRIND = valgrind --error-exitcode=42 --leak-check=full --show-leak-kinds=all .PRECIOUS: build/cleaf -.PHONY: all clean test ast-test semantic-test asan-test valgrind-test hir-test codegen-test +.PHONY: all clean test ast-test semantic-test asan-test valgrind-test hir-test hir-module-test codegen-test build-test integration-test setup all: $(BUILD)/cleaf $(BUILD)/cleaf: $(OBJ) $(CC) -o $@ $^ -lm - @$(BUILD)/cleaf test.clf $(BUILD)/%.o: $(SRC)/%.c @mkdir -p $(BUILD) @@ -44,6 +55,8 @@ $(BUILD)/%.o: $(SRC)/%.c @mkdir -p $(BUILD)/middleend @mkdir -p $(BUILD)/backend @mkdir -p $(BUILD)/compiler/definition + @mkdir -p $(BUILD)/compiler/setup + @mkdir -p $(BUILD)/compiler/build $(CC) $(CFLAGS) -c $< -o $@ AST_TEST_SRC = $(TEST)/ast_test.c @@ -55,15 +68,23 @@ SEM_TEST_BIN = $(BUILD)/semantic_test HIR_TEST_SRC = $(TEST)/hir_test.c HIR_TEST_BIN = $(BUILD)/hir_test +HIR_MODULE_TEST_SRC = $(TEST)/hir_module_test.c +HIR_MODULE_TEST_BIN = $(BUILD)/hir_module_test + CODEGEN_TEST_SRC = $(TEST)/codegen_test.c CODEGEN_TEST_BIN = $(BUILD)/codegen_test -test: $(AST_TEST_BIN) $(SEM_TEST_BIN) $(HIR_TEST_BIN) $(CODEGEN_TEST_BIN) +BUILD_TEST_SRC = $(TEST)/build_test.c +BUILD_TEST_BIN = $(BUILD)/build_test + +test: $(AST_TEST_BIN) $(SEM_TEST_BIN) $(HIR_TEST_BIN) $(HIR_MODULE_TEST_BIN) $(CODEGEN_TEST_BIN) $(BUILD_TEST_BIN) $(BUILD)/cleaf @echo "Running tests..." @$(AST_TEST_BIN) @$(SEM_TEST_BIN) @$(HIR_TEST_BIN) + @$(HIR_MODULE_TEST_BIN) @$(CODEGEN_TEST_BIN) + @$(BUILD_TEST_BIN) ast-test: $(AST_TEST_BIN) @echo "Running AST tests..." @@ -77,10 +98,22 @@ hir-test: $(HIR_TEST_BIN) @echo "Running hir tests..." @$(HIR_TEST_BIN) 2> test.log +hir-module-test: $(HIR_MODULE_TEST_BIN) + @echo "Running hir module (name mangling) tests..." + @$(HIR_MODULE_TEST_BIN) 2> test.log + codegen-test: $(CODEGEN_TEST_BIN) @echo "Running codegen tests..." @$(CODEGEN_TEST_BIN) 2> test.log +build-test: $(BUILD_TEST_BIN) + @echo "Running build tests..." + @$(BUILD_TEST_BIN) 2> test.log + +integration-test: $(BUILD)/cleaf + @echo "Running integration tests (cleaf build end-to-end)..." + @./test/integration_test.sh $(BUILD)/cleaf + $(AST_TEST_BIN): $(AST_TEST_SRC) $(SRC)/frontend/ast.c $(SRC)/thirdparty/error.c @mkdir -p $(BUILD) @$(CC) $(CFLAGS) $^ -o $@ -lm @@ -93,10 +126,18 @@ $(HIR_TEST_BIN): $(HIR_TEST_SRC) $(SRC)/frontend/ast.c $(SRC)/thirdparty/error.c @mkdir -p $(BUILD) @$(CC) $(CFLAGS) $^ -o $@ -lm +$(HIR_MODULE_TEST_BIN): $(HIR_MODULE_TEST_SRC) $(SRC)/frontend/ast.c $(SRC)/thirdparty/error.c $(SRC)/frontend/semantic.c $(SRC)/middleend/hir.c + @mkdir -p $(BUILD) + @$(CC) $(CFLAGS) $^ -o $@ -lm + $(CODEGEN_TEST_BIN): $(CODEGEN_TEST_SRC) $(SRC)/frontend/ast.c $(SRC)/thirdparty/error.c $(SRC)/frontend/semantic.c $(SRC)/middleend/hir.c $(SRC)/backend/x86_64.c $(SRC)/backend/codegen.c @mkdir -p $(BUILD) @$(CC) $(CFLAGS) $^ -o $@ -lm +$(BUILD_TEST_BIN): $(BUILD_TEST_SRC) $(SRC)/frontend/ast.c $(SRC)/thirdparty/error.c $(SRC)/frontend/semantic.c $(SRC)/middleend/hir.c $(SRC)/compiler/definition/compiler_definition.c $(SRC)/compiler/build/registry.c $(SRC)/compiler/build/export_table.c $(SRC)/compiler/build/import_resolver.c + @mkdir -p $(BUILD) + @$(CC) $(CFLAGS) $^ -o $@ -lm + asan-test: CFLAGS="-fsanitize=address,undefined -g -O1" make test @@ -127,8 +168,13 @@ valgrind-test: $(VALGRIND) ./build/cleaf test/valgrind_case/semantic_control_flow_errors.clf; [ $$? -ne 42 ] @echo "=== Testing combined errors ===" $(VALGRIND) ./build/cleaf test/valgrind_case/combined_multiple_errors.clf; [ $$? -ne 42 ] + @echo "=== Testing multi-module build ===" + cd test/integration_case/return_value_chain && rm -rf build a.out && $(VALGRIND) ../../../build/cleaf build; [ $$? -ne 42 ] @echo "=== All valgrind tests passed ===" clean: rm -rf $(BUILD) +setup: + chmod +x .githooks/pre-commit + git config core.hooksPath .githooks diff --git a/README.md b/README.md index 4241739..442dafa 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,11 @@ To compile `.clf` source files with the resulting binary: ```sh make # build ./build/cleaf -./build/cleaf # compile to a.out -./build/cleaf -o # compile with a custom output name +./build/cleaf # compile to build/a.out +./build/cleaf -o # compile with a custom output name (build/) ./build/cleaf -v # show each compilation phase and its result ./build/cleaf -V # same as -v, and dump AST, HIR, and generated assembly +./build/cleaf build # compile a multi-file module project (see below) ``` ## Examples @@ -95,6 +96,33 @@ fn main(): int { } ``` +### Modules and imports + +Multi-file projects use a Go/Rust-inspired module system, compiled with `cleaf build`: + +``` +// math.clf +module math + +internal fn helper(): int { return 41; } +fn add(): int { return helper(); } +``` + +``` +// main.clf +module main + +import math::add + +fn main(): int { + return add(); +} +``` + +`cleaf build` scans every `.clf` file in the current directory, resolves the module +dependency graph, and links everything into a single executable under `build/`. +`internal` functions are only visible within their own module. + ## Current state - [x] Lexer @@ -132,38 +160,53 @@ fn main(): int { - [x] Struct field access - [ ] Memory safety (garbage collection or ownership model, not yet decided) - [ ] Standard library -- [ ] Multiple source files +- [x] Multiple source files + - [x] `module`/`import` declarations (nested module paths via `::`) + - [x] `internal` visibility restriction + - [x] `cleaf build` — project-wide scan, dependency graph, topological compilation + - [x] Cross-module name mangling + multi-object codegen/link - [ ] Arrays - [ ] Additional primitive types ## Test coverage -The test suite contains 120 test cases totalling 248 assertions spread across the four compiler passes, -plus around 20 additional fixtures used for memory safety validation with Valgrind. +The test suite contains 249 test cases totalling 563 assertions spread across the compiler +passes and the module build pipeline, plus a set of end-to-end integration tests and +around 20 additional fixtures used for memory safety validation with Valgrind. -| Suite | Test cases | Assertions | -|----------|-----------|------------| -| Parser | 26 | 129 | -| Semantic | 56 | 85 | -| HIR | 20 | 20 | -| Codegen | 19 | 19 | -| **Total**| **120** | **248** | +| Suite | Test cases | Assertions | +|---------------------|-----------|------------| +| Parser (AST) | 64 | 324 | +| Semantic | 106 | 160 | +| HIR | 35 | 35 | +| HIR name mangling | 3 | 3 | +| Codegen | 34 | 34 | +| Build (imports) | 7 | 7 | +| **Total** | **249** | **563** | The semantic pass has the most coverage, reflecting the variety of error cases it handles. The parser and HIR passes cover the main language constructs. The codegen tests compare the full generated assembly output against expected fixtures for each construct. +On top of the suites above, `test/integration_case/` holds end-to-end multi-module +projects exercised via `make integration-test`: a correct 2-module build whose executable +is run and checked for the expected exit code, plus three failure scenarios (`internal` +violation, import cycle, missing `main` module). + Note that these numbers give a rough indication of coverage — there is no formal coverage measurement tool in place yet. ## Running tests ```sh -make test # run all test suites -make ast-test # parser tests only -make semantic-test # semantic analysis tests only -make hir-test # HIR lowering tests only -make codegen-test # code generation tests only -make asan-test # all tests with AddressSanitizer and UBSan -make valgrind-test # memory checks on a suite of ~20 .clf fixtures +make test # run all test suites, including integration tests +make ast-test # parser tests only +make semantic-test # semantic analysis tests only +make hir-test # HIR lowering tests only +make hir-module-test # HIR name mangling tests only +make codegen-test # code generation tests only +make build-test # multi-module import/semantic tests only +make integration-test # end-to-end `cleaf build` tests (requires nasm/ld) +make asan-test # all tests with AddressSanitizer and UBSan +make valgrind-test # memory checks on single-file and multi-module fixtures ``` diff --git a/cleaf_backlog.txt b/cleaf_backlog.txt index 30e837c..dd4f7d6 100644 --- a/cleaf_backlog.txt +++ b/cleaf_backlog.txt @@ -3,14 +3,14 @@ x 2026-06-08 2026-06-08 Add multiple int related types for better register usage x 2026-06-09 2026-06-09 Add "asm" function for inline assembly in the code base @global (B) Add "sizeof" compiler function @global (C) Add "type" keyword to create types at compile time @global -(A) Import workflow for multiple file and std compilation @global +x 2026-07-01 2026-07-01 Import workflow for multiple file and std compilation @global x 2026-06-11 2026-06-11 Add "char" type @global x 2026-06-18 2026-06-18 Add Array type @global (B) Add signed type @global x 2026-06-10 2026-06-10 Choose between mutable or const for var by default and change compiler behavior depending on this choice @global (A) Enhance error reporting @thirdparty x 2026-06-18 2026-06-18 choose if we pass struct var by value or pointer and implement both methods -(C) make language documentation @docs +x 2026-07-01 2026-07-01 make language documentation @docs (D) add defer keyword @global (A) add type casting @global (B) add way to get var address @global diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 7ec394d..c3bcee0 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -30,8 +30,8 @@ This produces `./build/cleaf`. ## Compiling a Cleaf program ```sh -./build/cleaf # compile to ./a.out -./build/cleaf -o # compile with a custom output name +./build/cleaf # compile to ./build/a.out +./build/cleaf -o # compile with a custom output name (./build/) ``` ### Debug flags @@ -55,7 +55,7 @@ Compile and run it: ```sh ./build/cleaf hello.clf -o hello -./hello +./build/hello echo $? # prints 0 ``` @@ -74,6 +74,15 @@ fn main(): int { ```sh ./build/cleaf example.clf -o example -./example +./build/example echo $? # prints 7 ``` + +## Multi-file projects + +Cleaf can also compile a project spread across several `.clf` files, using a +Go/Rust-inspired module system (`module`/`import` declarations). Running +`cleaf build` in a directory scans every `.clf` file, resolves dependencies between +modules, and produces a single executable at `build/a.out`. See +[Modules and Imports](/docs/language/modules) for the full syntax and rules. + diff --git a/docs/docs/language/modules.md b/docs/docs/language/modules.md new file mode 100644 index 0000000..790fa8a --- /dev/null +++ b/docs/docs/language/modules.md @@ -0,0 +1,125 @@ +--- +id: modules +title: Modules and Imports +sidebar_position: 10 +--- + +# Modules and Imports + +Cleaf supports splitting a program across multiple `.clf` files using a Go/Rust-inspired +module system, compiled with the `cleaf build` command. + +## Declaring a module + +Every file that is part of a multi-file project starts with a `module` declaration. +Module paths can be nested with `::`: + +```cleaf +// math.clf +module math + +fn add(int a, int b): int { + return a + b; +} +``` + +```cleaf +// io/console.clf +module std::io +``` + +Several files may declare the same module name — they are merged together, Go-style. + +## The `main` module + +Exactly one module named `main` must exist in the project, and it must contain a +`fn main(): int` — this is the program's entry point. `cleaf build` fails with an error +if no `main` module is found. + +## Importing symbols + +```cleaf +// main.clf +module main + +import math::add // call as: add(3, 4) or math::add(3, 4) +import math::add as madd // call as: madd(3, 4) + +fn main(): int { + var x = add(3, 4); + var y = math::add(1, 2); + return x; +} +``` + +- `import ::` brings a single function into scope. +- An optional `as ` renames it locally. +- At the call site, a qualifier (`math::add(...)`) is allowed and must match the last + segment of the imported module's path — at most one level of qualification is + supported (`io::print()`, not `std::io::print()`). + +## Restricting visibility with `internal` + +A function marked `internal` is only visible within its own module and cannot be +imported from anywhere else: + +```cleaf +module math + +internal fn helper(): int { + return 41; +} + +fn add(): int { + return helper(); // OK: same module +} +``` + +```cleaf +module main + +import math::helper // error: cannot import an internal function + +fn main(): int { + return helper(); +} +``` + +## Building a multi-module project + +```sh +cleaf build +``` + +`cleaf build` scans every `.clf` file in the current directory (recursively), resolves +the dependency graph between modules, and compiles them in dependency order (leaves +first). Compilation fails with an error if: + +- no `main` module is found, +- an import refers to an unknown module or symbol, +- an import targets an `internal` function from another module, +- the dependency graph contains a cycle. + +Each module is compiled to its own object file under `build/` (e.g. `build/math.o`, +`build/main.o`) and kept on disk after linking. The final executable is also written to +`build/` (`build/a.out` by default). + +## How module boundaries are erased + +Semantic analysis is the only compiler pass aware of module boundaries. Once a program +passes semantic analysis, function names are mangled into flat, globally unique symbols +before HIR lowering: + +``` +"main" in module "main" -> "start" (maps to the `_start` entry point) +"foo" in module "main" -> "main__foo" +"add" in module "math" -> "math__add" +"add" in module "std::io" -> "std__io__add" +``` + +From HIR lowering onward (codegen, assembly, linking), Cleaf only ever deals with these +mangled names and plain `global`/`extern` directives — there is no notion of "module" +below the semantic pass. + +Single-file compilation (`cleaf `, no `module` declaration) is unaffected: +function names are left untouched, exactly as before the module system was introduced. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index edcfc7c..4e13739 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -17,6 +17,7 @@ const sidebars: SidebarsConfig = { 'language/arrays', 'language/inline-asm', 'language/comments', + 'language/modules', ], }, ], diff --git a/sample/add.clf b/sample/add.clf new file mode 100644 index 0000000..e8a0a14 --- /dev/null +++ b/sample/add.clf @@ -0,0 +1,11 @@ +module math + +internal fn add(int a, int b): int { + return a + b; +} + +fn mul(int a, int b): int { + a = add(a, b); + return a * b; +} + diff --git a/src/backend/codegen.c b/src/backend/codegen.c index c19e0f9..72f45ad 100644 --- a/src/backend/codegen.c +++ b/src/backend/codegen.c @@ -38,6 +38,7 @@ int CODEGEN_write_function( if (strcmp(func->name, "main") == 0) { target->setup(sb); + target->emit_global(sb, "start"); target->func_write(sb, "start"); } else { diff --git a/src/backend/target.h b/src/backend/target.h index b82d9b5..fbce4cb 100644 --- a/src/backend/target.h +++ b/src/backend/target.h @@ -159,6 +159,14 @@ typedef struct { (*dealloc_memory) (string_builder_t*, const char* src, size_t size); + + void + (*emit_global) + (string_builder_t*, const char* name); + + void + (*emit_extern) + (string_builder_t*, const char* name); } target_t; #endif // TARGET_H diff --git a/src/backend/x86_64.c b/src/backend/x86_64.c index 8170caa..dd926ff 100644 --- a/src/backend/x86_64.c +++ b/src/backend/x86_64.c @@ -126,7 +126,7 @@ static void x86_emit_pop( static void x86_setup( string_builder_t* sb) { - sb_append_fmt(sb, "section .text\nglobal _start\n"); + sb_append_fmt(sb, "section .text\n"); } static void x86_func_write( @@ -318,6 +318,16 @@ static void x86_emit_mov_offset_post(string_builder_t* sb, sb_append_fmt(sb, " mov %s, [%s + %zu]\n", dst, src, size); } +static void x86_emit_global(string_builder_t* sb, const char* name) +{ + sb_append_fmt(sb, "global _%s\n", name); +} + +static void x86_emit_extern(string_builder_t* sb, const char* name) +{ + sb_append_fmt(sb, "extern _%s\n", name); +} + const target_t x86_64_target = { .setup = x86_setup, .regs_8 = x86_regs_8, @@ -361,4 +371,6 @@ const target_t x86_64_target = { .dealloc_memory = x86_dealloc_memory, .emit_load_elem = x86_emit_load_elem, .emit_store_elem = x86_emit_store_elem, + .emit_global = x86_emit_global, + .emit_extern = x86_emit_extern, }; diff --git a/src/cleaf.c b/src/cleaf.c index 85e3892..64cae65 100644 --- a/src/cleaf.c +++ b/src/cleaf.c @@ -14,255 +14,441 @@ #include "thirdparty/error.h" #include "frontend/semantic.h" #include "middleend/hir.h" +#include "thirdparty/rand.h" #include "backend/codegen.h" #include "backend/x86_64_definition.h" #include "compiler/definition/compiler_definition.h" +#include "compiler/setup/compiler_setup.h" +#include "compiler/build/registry.h" +#include "compiler/build/dep_graph.h" +#include "compiler/build/export_table.h" +#include "compiler/build/import_resolver.h" -int main(int argc, char** argv) +static char* build_object_basename(module_unit_t* unit) { - const char* filename = NULL; - const char* output = NULL; - log_verbosity_t verbosity = LOG_SILENT; - - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "-v") == 0) - verbosity = LOG_VERBOSE; - else if (strcmp(argv[i], "-V") == 0) - verbosity = LOG_DUMP; - else if (strcmp(argv[i], "-o") == 0) { - if (++i >= argc) { - error_report_general( - ERROR_SEVERITY_ERROR, "missing argument for '-o'"); - return 1; + if (unit->module_name) { + size_t len = strlen(unit->module_name); + char* out = malloc(len + 1); + if (!out) return NULL; + size_t j = 0; + for (size_t i = 0; i < len;) { + if (unit->module_name[i] == ':' && i + 1 < len && + unit->module_name[i + 1] == ':') { + out[j++] = '_'; + out[j++] = '_'; + i += 2; + } else { + out[j++] = unit->module_name[i++]; } - output = argv[i]; - } - else if (argv[i][0] != '-') - filename = argv[i]; - else { - error_report_general( - ERROR_SEVERITY_ERROR, "unknown flag '%s'", argv[i]); - fprintf( - stderr, "usage: %s [-v|-V] [-o ] \n", - argv[0]); - return 1; } + out[j] = '\0'; + return out; } - log_set_verbosity(verbosity); - - if (!filename) { - error_report_general( - ERROR_SEVERITY_ERROR, "no input file provided"); - fprintf( - stderr, "usage: %s [-v|-V] [-o ] \n", - argv[0]); - return 1; - } + const char* base = strrchr(unit->file_path, '/'); + base = base ? base + 1 : unit->file_path; + const char* dot = strrchr(base, '.'); + size_t len = dot ? (size_t)(dot - base) : strlen(base); + char* out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, base, len); + out[len] = '\0'; + return out; +} - log_phase("compiling", "'%s'", filename); +int main(int argc, char** argv) +{ + compiler_resources_t* res = NULL; - FILE *f = fopen(filename, "rb"); - if (f == NULL) { - error_report_general( - ERROR_SEVERITY_ERROR, "cannot open file '%s'", filename); - return 1; + if (argc > 1 && strcmp(argv[1], "build") == 0) { + res = build_setup(); + } else { + res = single_file_setup(argc, argv); } - compiler_resources_t res = {0}; - res.text = (char *) malloc(1 << 20); - int len = (int) fread(res.text, 1, 1 << 20, f); - fclose(f); - if (len < 0) { - error_report_general( - ERROR_SEVERITY_ERROR, "failed to read file '%s'", filename); - compiler_resources_free(&res); - return 1; - } + if (!res) return 1; - error_context_t error_ctx; - error_init(&error_ctx, filename, res.text, len); + int is_build_mode = (argc > 1 && strcmp(argv[1], "build") == 0); - lexer_t lex; - res.parser.error_ctx = &error_ctx; + log_phase("compiling", "%zu file(s)", res->files.count); - lexer_init_lexer( - &lex, res.text, res.text + len, (char*) malloc(4096), 4096); + build_context_t build_ctx = {0}; + if (is_build_mode) { + build_ctx.registry = calloc(1, sizeof(hashmap_t)); + if (!build_ctx.registry) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + compiler_resources_free(res); + return 1; + } + } - while (lexer_get_token(&lex)) { - if (lex.token == LEXER_token_parse_error) { + da_foreach(char*, it, &res->files) { + char* filename = *it; + + FILE* f = fopen(filename, "rb"); + if (!f) { error_report_general( - ERROR_SEVERITY_ERROR, "lexer parse error"); - free(lex.string_storage); - compiler_resources_free(&res); + ERROR_SEVERITY_ERROR, "cannot open file '%s'", filename); + compiler_resources_free(res); + build_context_free(&build_ctx); return 1; } - token_t t = lexer_copy_token(&lex); - lexer_print_token(&lex); - printf(" "); - da_append(&res.parser, t); - } - free(lex.string_storage); - log_phase("lexing", "%zu tokens", res.parser.count); + module_unit_t* unit = calloc(1, sizeof(module_unit_t)); + if (!unit) { + fclose(f); + compiler_resources_free(res); + build_context_free(&build_ctx); + return 1; + } - res.parser.types = calloc(1, sizeof(known_type_array)); - if (!res.parser.types) { - error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); - return 1; - } + unit->file_path = filename; + unit->source = malloc(1 << 20); + unit->source_len = (int) fread(unit->source, 1, 1 << 20, f); + fclose(f); + + error_init(&unit->error_ctx, filename, unit->source, unit->source_len); + unit->parser.error_ctx = &unit->error_ctx; + + lexer_t lex; + lexer_init_lexer( + &lex, unit->source, unit->source + unit->source_len, + malloc(4096), 4096); + + while (lexer_get_token(&lex)) { + if (lex.token == LEXER_token_parse_error) { + error_report_general(ERROR_SEVERITY_ERROR, "lexer parse error"); + free(lex.string_storage); + module_unit_free(unit); + compiler_resources_free(res); + build_context_free(&build_ctx); + return 1; + } + token_t t = lexer_copy_token(&lex); + da_append(&unit->parser, t); + } + free(lex.string_storage); - populate_parser_known_type(res.parser.types); + log_phase("lexing", "'%s': %zu tokens", filename, unit->parser.count); - while ((size_t) res.parser.pos < res.parser.count) { - declaration_t* decl = parse_declaration(&res.parser); - if (decl == NULL) { - error_report_general(ERROR_SEVERITY_ERROR, "ast parse error"); - compiler_resources_free(&res); + unit->parser.types = calloc(1, sizeof(known_type_array)); + if (!unit->parser.types) { + module_unit_free(unit); + compiler_resources_free(res); + build_context_free(&build_ctx); return 1; } - da_append(&res.program, decl); + populate_parser_known_type(unit->parser.types); + + while ((size_t) unit->parser.pos < unit->parser.count) { + declaration_t* decl = parse_declaration(&unit->parser); + if (!decl) { + error_report_general( + ERROR_SEVERITY_ERROR, + "ast parse error in '%s'", filename); + module_unit_free(unit); + compiler_resources_free(res); + build_context_free(&build_ctx); + return 1; + } + da_append(&unit->program, decl); + } + + log_phase("parsing", "'%s': %zu declaration(s)", filename, unit->program.count); + + if (log_is_dump()) { + log_section_begin("AST"); + ast_print_program(&unit->program); + log_section_end(); + } + + da_append(&res->units, unit); } - log_phase("parsing", "%zu declaration(s)", res.program.count); + if (is_build_mode) { + da_foreach(module_unit_t*, it, &res->units) { + if (!populate_module_registry(&build_ctx, *it)) { + error_report_general( + ERROR_SEVERITY_ERROR, + "error while building module registry"); + compiler_resources_free(res); + build_context_free(&build_ctx); + return 1; + } + } + + module_unit_array* main_units = + (module_unit_array*) hashmap_get(build_ctx.registry, "main"); - if (log_is_dump()) { - log_section_begin("AST"); - ast_print_program(&res.program); - log_section_end(); + if (!main_units || main_units->count == 0) { + build_context_free(&build_ctx); + error_report_general(ERROR_SEVERITY_ERROR, + "no `main` module found"); + compiler_resources_free(res); + return 1; + } } - semantic_analyzer_t analyzer = {0}; - analyzer.error_ctx = &error_ctx; - analyzer.ast = &res.program; - analyzer.error_count = 0; + if (is_build_mode) { + if (!build_dep_graph(&build_ctx)) { + build_context_free(&build_ctx); + compiler_resources_free(res); + return 1; + } - semantic_analyze(&analyzer); + log_phase("topo order", "%zu module(s)", build_ctx.count); + for (size_t i = 0; i < build_ctx.count; ++i) + log_phase(" -->", "%s", build_ctx.items[i]->module_name); + } else { + da_foreach(module_unit_t*, it, &res->units) { + da_append(&build_ctx, *it); + } + } - if (log_is_dump()) { - log_section_begin("AST after semantic"); - ast_print_program(&res.program); - log_section_end(); + da_foreach(module_unit_t*, it, &build_ctx) { + if (!semantic_build_export_table(*it)) { + build_context_free(&build_ctx); + compiler_resources_free(res); + return 1; + } } - if (analyzer.error_count > 0) { - log_phase("semantic", "%d error(s)", analyzer.error_count); - error_report_general( - ERROR_SEVERITY_NOTE, - "%d error(s) during semantic analysis, aborting", - analyzer.error_count); - semantic_free_program_definition(&analyzer); - compiler_resources_free(&res); + res->hir_program = calloc(1, sizeof(IR_function_array)); + if (!res->hir_program) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + build_context_free(&build_ctx); + compiler_resources_free(res); return 1; } - log_phase("semantic", "ok"); - res.hir_program = calloc(1, sizeof(IR_function_array)); - if (!res.hir_program) { - error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); - compiler_resources_free(&res); + rand_t chunk_rng; + rand_init(&chunk_rng); + + const target_t* target = &x86_64_target; + compiled_files_array object_files = {0}; + + if (system("mkdir -p build") != 0) { + error_report_general(ERROR_SEVERITY_ERROR, "cannot create 'build' directory"); + build_context_free(&build_ctx); + compiler_resources_free(res); return 1; } - HIR_parser_t hir_parser = {0}; - hir_parser.error_ctx = &error_ctx; - hir_parser.error_count = 0; - hir_parser.hir_program = res.hir_program; - hir_parser.struct_symbols = analyzer.struct_symbols; - rand_t rng; - rand_init(&rng); - HIR_PARSER_USE_RNG(hir_parser, &rng); - - da_foreach(declaration_t*, it, &res.program) { - if ((*it)->type != DECLARATION_FUNC) + int had_errors = 0; + da_foreach(module_unit_t*, it, &build_ctx) { + module_unit_t* unit = *it; + + semantic_analyzer_t analyzer = {0}; + analyzer.error_ctx = &unit->error_ctx; + analyzer.ast = &unit->program; + + if (!semantic_resolve_imports(&build_ctx, unit, &analyzer)) { + semantic_free_program_definition(&analyzer); + build_context_free(&build_ctx); + compiler_resources_free(res); + return 1; + } + + log_phase("semantic", "'%s' (module '%s')", + unit->file_path, unit->module_name ? unit->module_name : "-"); + semantic_analyze(&analyzer); + + if (analyzer.error_count > 0) { + had_errors = 1; + semantic_free_program_definition(&analyzer); continue; + } + + HIR_parser_t hir_parser = {0}; + hir_parser.error_ctx = &unit->error_ctx; + hir_parser.hir_program = res->hir_program; + hir_parser.struct_symbols = analyzer.struct_symbols; + hir_parser.current_module = unit->module_name; + HIR_PARSER_USE_RNG(hir_parser, &chunk_rng); + + size_t hir_before = res->hir_program->count; + da_foreach(declaration_t*, dit, &unit->program) { + if (IR_lower_function(&hir_parser, *dit) != 0) { + error_report_general( + ERROR_SEVERITY_ERROR, "hir lowering error in '%s'", unit->file_path); + had_errors = 1; + break; + } + } + + log_phase("hir", "'%s' (module '%s'): %zu function(s)", + unit->file_path, unit->module_name ? unit->module_name : "-", + res->hir_program->count - hir_before); + + if (log_is_dump()) { + log_section_begin("HIR"); + for (size_t i = hir_before; i < res->hir_program->count; ++i) { + char* hir_text = IR_generate_string_program(res->hir_program->items[i]); + fprintf(stderr, "%s", hir_text); + free(hir_text); + } + log_section_end(); + } + + string_builder_t module_sb = {0}; + if (unit->module_name) { + target->setup(&module_sb); + + da_foreach(declaration_t*, dit2, &unit->program) { + declaration_t* decl = *dit2; + if (decl->type != DECLARATION_FUNC) continue; + + char* mangled = + IR_mangle_function_name(unit->module_name, decl->func.name); + if (!mangled) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + had_errors = 1; + continue; + } + + if (!decl->func.is_internal || strcmp(mangled, "start") == 0) + target->emit_global(&module_sb, mangled); + + free(mangled); + } + + compiled_files_array externs_emitted = {0}; + for (size_t i = hir_before; i < res->hir_program->count; ++i) { + IR_function_t* f = res->hir_program->items[i]; + da_foreach(IR_instruction_t*, cit, f->code) { + if ((*cit)->kind != IR_CALL) continue; + const char* callee = (*cit)->func_name; + + bool is_local = false; + for (size_t k = hir_before; k < res->hir_program->count; ++k) { + if (strcmp(res->hir_program->items[k]->name, callee) == 0) { + is_local = true; + break; + } + } + if (is_local) continue; + + bool already_emitted = false; + da_foreach(char*, eit, &externs_emitted) { + if (strcmp(*eit, callee) == 0) { + already_emitted = true; + break; + } + } + if (already_emitted) continue; + + target->emit_extern(&module_sb, callee); + da_append(&externs_emitted, strdup(callee)); + } + } + da_foreach(char*, eit, &externs_emitted) free(*eit); + da_free(&externs_emitted); + } + + int codegen_error = 0; + for (size_t i = hir_before; i < res->hir_program->count; ++i) { + if (CODEGEN_write_function(&module_sb, res->hir_program->items[i], target) != 0) { + codegen_error = 1; + break; + } + } + + log_phase("codegen", "'%s' (module '%s'): %zu byte(s) of assembly", + unit->file_path, unit->module_name ? unit->module_name : "-", + module_sb.count); - int lowering_result = IR_lower_function(&hir_parser, *it); - if (lowering_result != 0) { + if (codegen_error) { error_report_general( - ERROR_SEVERITY_ERROR, "HIR lowering error"); - compiler_resources_free(&res); - return 1; + ERROR_SEVERITY_ERROR, "codegen error in '%s'", unit->file_path); + had_errors = 1; + da_free(&module_sb); + semantic_free_program_definition(&analyzer); + continue; + } + + char* base = build_object_basename(unit); + if (!base) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + had_errors = 1; + da_free(&module_sb); + semantic_free_program_definition(&analyzer); + continue; } - } - log_phase( - "HIR lowering", "%zu function(s)", res.hir_program->count); + char asm_path[512]; + snprintf(asm_path, sizeof(asm_path), "build/%s.asm", base); + char* obj_path = malloc(strlen("build/") + strlen(base) + strlen(".o") + 1); + sprintf(obj_path, "build/%s.o", base); + free(base); - semantic_free_program_definition(&analyzer); - if (log_is_dump()) { - log_section_begin("HIR"); - da_foreach(IR_function_t*, it, hir_parser.hir_program) { - IR_display_function(*it); + FILE* asm_f = fopen(asm_path, "wb"); + if (!asm_f) { + error_report_general( + ERROR_SEVERITY_ERROR, "cannot write asm file '%s'", asm_path); + had_errors = 1; + free(obj_path); + da_free(&module_sb); + semantic_free_program_definition(&analyzer); + continue; } - log_section_end(); - } + fwrite(module_sb.items, 1, module_sb.count, asm_f); + fclose(asm_f); + da_free(&module_sb); - string_builder_t asm_prog = {0}; - da_foreach(IR_function_t*, it, hir_parser.hir_program) { - int err = CODEGEN_write_function(&asm_prog, *it, &x86_64_target); - if (err) { - da_free(&asm_prog); - compiler_resources_free(&res); - return 1; + char nasm_cmd[1200]; + snprintf(nasm_cmd, sizeof(nasm_cmd), + "nasm -f elf64 %s -o %s", asm_path, obj_path); + + log_phase("assemble", "'%s' -> '%s'", asm_path, obj_path); + + if (system(nasm_cmd) != 0) { + error_report_general( + ERROR_SEVERITY_ERROR, + "nasm failed to assemble '%s'", asm_path); + had_errors = 1; + free(obj_path); + semantic_free_program_definition(&analyzer); + continue; } - } - log_phase("codegen", "ok"); + da_append(&object_files, obj_path); - if (log_is_dump()) { - log_section_begin("ASM"); - printf("%s", asm_prog.items); - log_section_end(); + semantic_free_program_definition(&analyzer); } - const char* output_name = output ? output : "a.out"; + if (!had_errors && object_files.count > 0) { + string_builder_t link_cmd = {0}; + sb_append_fmt(&link_cmd, "ld"); + da_foreach(char*, oit, &object_files) { + sb_append_fmt(&link_cmd, " %s", *oit); + } - const char* obj_base = strrchr(output_name, '/'); - obj_base = obj_base ? obj_base + 1 : output_name; + const char* requested = res->output ? res->output : "a.out"; + char output_path[512]; + if (strchr(requested, '/') != NULL) + snprintf(output_path, sizeof(output_path), "%s", requested); + else + snprintf( + output_path, sizeof(output_path), "build/%s", requested); - char asm_path[512]; - snprintf(asm_path, sizeof(asm_path), "/tmp/%s.asm", obj_base); - FILE* asm_file = fopen(asm_path, "w"); - if (!asm_file) { - error_report_general(ERROR_SEVERITY_ERROR, "failed to write temp asm file"); - da_free(&asm_prog); - compiler_resources_free(&res); - return 1; - } - fputs(asm_prog.items, asm_file); - fclose(asm_file); - da_free(&asm_prog); - - char nasm_cmd[1024]; - snprintf(nasm_cmd, sizeof(nasm_cmd), - "nasm -f elf64 -o /tmp/%s.o %s", obj_base, asm_path); - if (system(nasm_cmd) != 0) { - error_report_general(ERROR_SEVERITY_ERROR, "nasm failed"); - compiler_resources_free(&res); - return 1; - } - remove(asm_path); + sb_append_fmt(&link_cmd, " -o %s", output_path); - log_phase("nasm", "ok"); + log_phase("link", "%zu object file(s) -> '%s'", + object_files.count, output_path); - char ld_cmd[1024]; - snprintf( - ld_cmd, sizeof(ld_cmd), "ld -o %s /tmp/%s.o", - output_name, obj_base); + if (system(link_cmd.items) != 0) { + error_report_general(ERROR_SEVERITY_ERROR, "linking failed"); + had_errors = 1; + } - if (system(ld_cmd) != 0) { - error_report_general(ERROR_SEVERITY_ERROR, "ld failed"); - compiler_resources_free(&res); - return 1; + da_free(&link_cmd); } - char rm_cmd[512]; - snprintf(rm_cmd, sizeof(rm_cmd), "rm /tmp/%s.o", obj_base); - system(rm_cmd); - - log_phase("linking", "'%s'", output_name); + da_foreach(char*, oit, &object_files) free(*oit); + da_free(&object_files); - compiler_resources_free(&res); - return 0; + build_context_free(&build_ctx); + compiler_resources_free(res); + return had_errors ? 1 : 0; } + diff --git a/src/compiler/build/dep_graph.c b/src/compiler/build/dep_graph.c new file mode 100644 index 0000000..f62d3de --- /dev/null +++ b/src/compiler/build/dep_graph.c @@ -0,0 +1,127 @@ +#include "compiler/build/dep_graph.h" + +static void dep_graph_free(dep_graph_t* graph) +{ + da_foreach(dep_node_t, it, graph) { + da_free(it); + } + da_free(graph); + if (graph->index) { + hashmap_free(graph->index, 0); + free(graph->index); + graph->index = NULL; + } +} + +static void topo_visit(build_context_t* ctx, dep_node_t* node, bool* had_cycle) +{ + if (node->color == BLACK) return; + + if (node->color == GRAY) { + error_report_general(ERROR_SEVERITY_ERROR, + "circular dependency detected involving module '%s'", + node->module_name); + *had_cycle = true; + return; + } + + node->color = GRAY; + + da_foreach(dep_node_t*, it, node) { + topo_visit(ctx, (*it), had_cycle); + } + + node->color = BLACK; + module_unit_array* arr = hashmap_get(ctx->registry, node->module_name); + if (arr) { + da_foreach(module_unit_t*, it, arr) { + da_append(ctx, (*it)); + } + } +} + +bool build_dep_graph(build_context_t* ctx) +{ + dep_graph_t graph = {0}; + + graph.index = calloc(1, sizeof(hashmap_t)); + if (!graph.index) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return false; + } + + for (size_t i = 0; i < HASH_SIZE; ++i) { + hashmap_entry_t* e = ctx->registry->buckets[i]; + + while (e) { + dep_node_t node = {0}; + node.module_name = e->key; + node.color = WHITE; + + da_append(&graph, node); + + e = e->next; + } + } + + da_foreach(dep_node_t, it, &graph) { + hashmap_put(graph.index, it->module_name, it); + } + + for (size_t i = 0; i < HASH_SIZE; ++i) { + hashmap_entry_t* e = ctx->registry->buckets[i]; + while (e) { + module_unit_array* mod = (module_unit_array*) e->value; + + da_foreach(module_unit_t*, it, mod) { + module_unit_t* unit = (*it); + + da_foreach(declaration_t*, it, &unit->program) { + declaration_t* decl = (*it); + + if (decl->type != DECLARATION_IMPORT) + continue; + + char* import_name = calloc(255, sizeof(char)); + if (!import_name) { dep_graph_free(&graph); return false; } + + for (size_t j = 0; j < decl->import.path.count - 1; ++j) { + if (import_name[0] == '\0') { + strcpy(import_name, decl->import.path.items[j]); + } + else { + strcat(strcat(import_name, "::"), decl->import.path.items[j]); + } + } + + dep_node_t* src_node = + hashmap_get(graph.index, unit->module_name); + dep_node_t* dst_node = + hashmap_get(graph.index, import_name); + + if (!dst_node) { + error_report_general(ERROR_SEVERITY_ERROR, + "unkown imported module %s\n", import_name); + free(import_name); + dep_graph_free(&graph); + return false; + } + + da_append(src_node, dst_node); + free(import_name); + } + } + + e = e->next; + } + } + + bool had_cycle = false; + da_foreach(dep_node_t, it, &graph) { + if (strcmp(it->module_name, "main") == 0) + topo_visit(ctx, it, &had_cycle); + } + + dep_graph_free(&graph); + return !had_cycle; +} diff --git a/src/compiler/build/dep_graph.h b/src/compiler/build/dep_graph.h new file mode 100644 index 0000000..6555152 --- /dev/null +++ b/src/compiler/build/dep_graph.h @@ -0,0 +1,33 @@ +#ifndef DEP_GRAPH_H +#define DEP_GRAPH_H + +#include +#include "compiler/definition/compiler_definition.h" +#define DA_LIB_IMPLEMENTATION +#include "thirdparty/da.h" +#include "thirdparty/hashmap.h" + +typedef enum { + WHITE = 0, + GRAY = 1, + BLACK = 2, +} visit_color_t; + +typedef struct dep_node_t { + char* module_name; // not owned + visit_color_t color; + struct dep_node_t** items; // act as 'arcs' + size_t count; // we use items, count, capacity + size_t capacity; // so we can use da.h interface +} dep_node_t; + +typedef struct { + dep_node_t* items; + size_t count; + size_t capacity; + hashmap_t* index; +} dep_graph_t; + +bool build_dep_graph(build_context_t* ctx); + +#endif // DEP_GRAPH_H diff --git a/src/compiler/build/export_table.c b/src/compiler/build/export_table.c new file mode 100644 index 0000000..aa262ab --- /dev/null +++ b/src/compiler/build/export_table.c @@ -0,0 +1,51 @@ +#include "export_table.h" +#include "frontend/symbols.h" +#include "thirdparty/error.h" + +bool semantic_build_export_table(module_unit_t* unit) +{ + unit->export_funcs = calloc(1, sizeof(hashmap_t)); + if (!unit->export_funcs) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return false; + } + + da_foreach(declaration_t*, it, &unit->program) { + declaration_t* decl = *it; + if (decl->type != DECLARATION_FUNC) continue; + if (decl->func.is_internal) continue; + + function_symbol_t* fs = calloc(1, sizeof(function_symbol_t)); + if (!fs) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return false; + } + + fs->return_type.kind = decl->func.return_type.kind; + fs->params_count = decl->func.params.count; + + if (fs->params_count > 0) { + fs->params_name = calloc(fs->params_count, sizeof(char*)); + fs->params_type = + calloc(fs->params_count, sizeof(variable_symbol_t)); + + if (!fs->params_name || !fs->params_type) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free(fs->params_name); + free(fs->params_type); + free(fs); + return false; + } + for (size_t i = 0; i < fs->params_count; ++i) { + fs->params_name[i] = decl->func.params.items[i].ident_name; + fs->params_type[i].type = decl->func.params.items[i].type; + fs->params_type[i].is_constant = + decl->func.params.items[i].is_constant; + } + } + + hashmap_put(unit->export_funcs, decl->func.name, fs); + } + + return true; +} diff --git a/src/compiler/build/export_table.h b/src/compiler/build/export_table.h new file mode 100644 index 0000000..2bd7e4e --- /dev/null +++ b/src/compiler/build/export_table.h @@ -0,0 +1,9 @@ +#ifndef EXPORT_TABLE_H +#define EXPORT_TABLE_H + +#include +#include "compiler/definition/compiler_definition.h" + +bool semantic_build_export_table(module_unit_t* unit); + +#endif // EXPORT_TABLE_H diff --git a/src/compiler/build/file_scanner.c b/src/compiler/build/file_scanner.c new file mode 100644 index 0000000..11daa1d --- /dev/null +++ b/src/compiler/build/file_scanner.c @@ -0,0 +1,68 @@ +#include "compiler/build/file_scanner.h" + +bool read_files_in_dir( + compiled_files_array* files, char* path) +{ + DIR* dir; + struct dirent* entry; + + if ((dir = opendir(path)) == NULL) { + return false; + } + + while((entry = readdir(dir)) != NULL) { + if (entry->d_type == 8) { + char* extension = strrchr(entry->d_name, '.'); + if (!extension) + continue; + + if (strcmp(extension, ".clf") == 0) { + char* file = + calloc( + strlen(path) + strlen(entry->d_name) + 2, + sizeof(char)); + file = + strcat(strcat(strcpy(file, path), "/"), entry->d_name); + da_append(files, file); + } + } + else if (entry->d_type == 4) { + // for now, we skip some dirs for no other reason than convinience + // TODO: maybe add `cleaf test` command + if (strcmp(entry->d_name, "build") == 0 || + strcmp(entry->d_name, "test") == 0 || + strcmp(entry->d_name, "docs") == 0 || + strcmp(entry->d_name, ".git") == 0 || + strcmp(entry->d_name, ".github") == 0 || + strcmp(entry->d_name, "..") == 0 || + strcmp(entry->d_name, ".") == 0) { + continue; + } + + char* new = + calloc( + strlen(path) + strlen(entry->d_name) + 2, sizeof(char)); + + read_files_in_dir( + files, + strcat(strcat(strcpy(new, path), "/"), entry->d_name)); + } + } + + free(path); + closedir(dir); + return true; +} + +compiled_files_array find_source_files() +{ + compiled_files_array files = {0}; + + char* path = calloc(2, sizeof(char)); + path = strcpy(path, "."); + if(!read_files_in_dir(&files, path)) { + return files; + } + + return files; +} diff --git a/src/compiler/build/file_scanner.h b/src/compiler/build/file_scanner.h new file mode 100644 index 0000000..e159caf --- /dev/null +++ b/src/compiler/build/file_scanner.h @@ -0,0 +1,13 @@ +#ifndef BUILD_SCANNER_H +#define BUILD_SCANNER_H + +#include +#include + +#include "compiler/definition/compiler_definition.h" + +compiled_files_array find_source_files(); +bool read_files_in_dir( + compiled_files_array* files, char* path); + +#endif // BUILD_SCANNER_H diff --git a/src/compiler/build/import_resolver.c b/src/compiler/build/import_resolver.c new file mode 100644 index 0000000..920743e --- /dev/null +++ b/src/compiler/build/import_resolver.c @@ -0,0 +1,141 @@ +#include "import_resolver.h" +#include "frontend/symbols.h" +#include "thirdparty/error.h" +#include +#include + +static char* join_path_segments(char** segs, size_t count) +{ + size_t len = 1; + for (size_t i = 0; i < count; ++i) + len += strlen(segs[i]) + 2; + + char* out = calloc(len, sizeof(char)); + if (!out) return NULL; + + for (size_t i = 0; i < count; ++i) { + if (i > 0) strcat(out, "::"); + strcat(out, segs[i]); + } + + return out; +} + +static bool declares_internal_symbol( + module_unit_t* unit, const char* name) +{ + da_foreach(declaration_t*, it, &unit->program) { + declaration_t* d = *it; + if (d->type == DECLARATION_FUNC && + d->func.is_internal && + strcmp(d->func.name, name) == 0) + return true; + } + + return false; +} + +static function_symbol_t* find_exported_symbol( + module_unit_array* units, + const char* name, + bool* out_is_internal) +{ + *out_is_internal = false; + + da_foreach(module_unit_t*, it, units) { + module_unit_t* u = *it; + + function_symbol_t* fs = + (function_symbol_t*) hashmap_get(u->export_funcs, name); + if (fs) return fs; + + if (declares_internal_symbol(u, name)) + *out_is_internal = true; + } + + return NULL; +} + +bool semantic_resolve_imports( + build_context_t* ctx, + module_unit_t* unit, + semantic_analyzer_t* analyzer) +{ + analyzer->imported_functions = calloc(1, sizeof(hashmap_t)); + if (!analyzer->imported_functions) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return false; + } + + da_foreach(declaration_t*, it, &unit->program) { + declaration_t* decl = *it; + if (decl->type != DECLARATION_IMPORT) continue; + + import_path_t* path = &decl->import.path; + if (path->count < 2) { + semantic_error_register(analyzer, decl->source_pos - 1, + "import path must reference `module::symbol`"); + continue; + } + + const char* symbol_name = path->items[path->count - 1]; + const char* qualifier = path->items[path->count - 2]; + + char* module_name = join_path_segments(path->items, path->count - 1); + if (!module_name) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return false; + } + + module_unit_array* target_units = + (module_unit_array*) hashmap_get(ctx->registry, module_name); + + if (!target_units || target_units->count == 0) { + semantic_error_register(analyzer, decl->source_pos - 1, + "unknown imported module"); + free(module_name); + continue; + } + + bool is_internal = false; + function_symbol_t* fs = + find_exported_symbol(target_units, symbol_name, &is_internal); + + if (!fs) { + semantic_error_register(analyzer, decl->source_pos - 1, + is_internal + ? "cannot import an internal function" + : "undefined symbol in imported module"); + free(module_name); + continue; + } + + imported_symbol_t* isym = calloc(1, sizeof(imported_symbol_t)); + if (!isym) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free(module_name); + return false; + } + + isym->fs = fs; + isym->module_name = module_name; + isym->qualifier = strdup(qualifier); + + const char* local_name = + decl->import.alias ? decl->import.alias : symbol_name; + + hashmap_put(analyzer->imported_functions, local_name, isym); + + size_t qk_len = strlen(qualifier) + 2 + strlen(symbol_name) + 1; + char* qualified_key = malloc(qk_len); + if (qualified_key) { + snprintf(qualified_key, qk_len, "%s::%s", qualifier, symbol_name); + hashmap_put(analyzer->imported_functions, qualified_key, isym); + free(qualified_key); + } + + da_append(&analyzer->imported_owned, isym); + } + + return true; +} diff --git a/src/compiler/build/import_resolver.h b/src/compiler/build/import_resolver.h new file mode 100644 index 0000000..fe34d06 --- /dev/null +++ b/src/compiler/build/import_resolver.h @@ -0,0 +1,13 @@ +#ifndef IMPORT_RESOLVER_H +#define IMPORT_RESOLVER_H + +#include +#include "compiler/definition/compiler_definition.h" +#include "frontend/semantic.h" + +bool semantic_resolve_imports( + build_context_t* ctx, + module_unit_t* unit, + semantic_analyzer_t* analyzer); + +#endif // IMPORT_RESOLVER_H diff --git a/src/compiler/build/registry.c b/src/compiler/build/registry.c new file mode 100644 index 0000000..0d1fadb --- /dev/null +++ b/src/compiler/build/registry.c @@ -0,0 +1,49 @@ +#include "compiler/build/registry.h" +#include + +static char* build_module_key(declaration_t* module_decl) +{ + char* key = NULL; + da_foreach(char*, seg, &(module_decl->module.path)) { + if (key) { + size_t new_len = strlen(key) + 2 + strlen(*seg) + 1; + key = realloc(key, new_len); + if (!key) return NULL; + strcat(strcat(key, "::"), *seg); + } else { + key = calloc(strlen(*seg) + 1, sizeof(char)); + if (!key) return NULL; + strcpy(key, *seg); + } + } + return key; +} + +bool populate_module_registry( + build_context_t* ctx, module_unit_t* unit) +{ + da_foreach(declaration_t*, it, &(unit->program)) { + declaration_t* d = *it; + if (d->type != DECLARATION_MODULE) + continue; + + char* key = build_module_key(d); + if (!key) return false; + + unit->module_name = strdup(key); + if (!unit->module_name) { free(key); return false; } + + module_unit_array* arr = hashmap_get(ctx->registry, key); + if (!arr) { + arr = calloc(1, sizeof(module_unit_array)); + if (!arr) { free(key); return false; } + hashmap_put(ctx->registry, key, arr); + } + + da_append(arr, unit); + free(key); + return true; + } + + return false; +} diff --git a/src/compiler/build/registry.h b/src/compiler/build/registry.h new file mode 100644 index 0000000..e18172d --- /dev/null +++ b/src/compiler/build/registry.h @@ -0,0 +1,13 @@ +#ifndef REGISTRY_H +#define REGISTRY_H + +#include + +#include "compiler/definition/compiler_definition.h" +#include "thirdparty/hashmap.h" +#include "frontend/ast_definition.h" + +bool populate_module_registry( + build_context_t* ctx, module_unit_t* unit); + +#endif // REGISTRY_H diff --git a/src/compiler/definition/compiler_definition.c b/src/compiler/definition/compiler_definition.c index 47b349e..b931a30 100644 --- a/src/compiler/definition/compiler_definition.c +++ b/src/compiler/definition/compiler_definition.c @@ -1,14 +1,63 @@ #include "compiler_definition.h" +#include "frontend/symbols.h" -void compiler_resources_free(compiler_resources_t* res) +void module_unit_free(module_unit_t* unit) { - da_foreach(known_type_t, it, res->parser.types) { - if (it->kind == TYPE_CUSTOM) - if (it->name) + if (!unit) return; + + if (unit->parser.types) { + da_foreach(known_type_t, it, unit->parser.types) { + if (it->kind == TYPE_CUSTOM && it->name) free(it->name); + } + da_free(unit->parser.types); + free(unit->parser.types); + } + + for (size_t i = 0; i < unit->parser.count; i++) { + if (unit->parser.items[i].string_value) + free(unit->parser.items[i].string_value); + } + da_free(&unit->parser); + + da_foreach(declaration_t*, it, &unit->program) { + free_declaration(*it); + } + da_free(&unit->program); + + free(unit->module_name); + free(unit->source); + + if (unit->export_funcs) { + for (size_t i = 0; i < 211; ++i) { + hashmap_entry_t* e = unit->export_funcs->buckets[i]; + while (e) { + function_symbol_t* fs = (function_symbol_t*) e->value; + free(fs->params_name); + free(fs->params_type); + e = e->next; + } + } + hashmap_free(unit->export_funcs, 1); + free(unit->export_funcs); + } + + free(unit); +} + +void compiler_resources_free(compiler_resources_t* res) +{ + if (!res) return; + + da_foreach(module_unit_t*, it, &res->units) { + module_unit_free(*it); + } + da_free(&res->units); + + da_foreach(char*, it, &res->files) { + free(*it); } - da_free(res->parser.types); - free(res->parser.types); + da_free(&res->files); if (res->hir_program) { da_foreach(IR_function_t*, it, res->hir_program) { @@ -19,18 +68,30 @@ void compiler_resources_free(compiler_resources_t* res) res->hir_program = NULL; } - da_foreach(declaration_t*, it, &res->program) { - free_declaration(*it); - } - da_free(&res->program); + free(res); +} - for (size_t i = 0; i < res->parser.count; i++) { - if (res->parser.items[i].string_value) - free(res->parser.items[i].string_value); +void build_context_free(build_context_t* ctx) +{ + if (ctx->items) { + da_free(ctx); + ctx->items = NULL; } - da_free(&res->parser); - free(res->text); - res->text = NULL; + if (ctx->registry) { + for (size_t i = 0; i < HASH_SIZE; i++) { + hashmap_entry_t* e = ctx->registry->buckets[i]; + while (e) { + module_unit_array* arr = (module_unit_array*) e->value; + if (arr) { + da_free(arr); + free(arr); + } + e = e->next; + } + } + hashmap_free(ctx->registry, 0); + free(ctx->registry); + ctx->registry = NULL; + } } - diff --git a/src/compiler/definition/compiler_definition.h b/src/compiler/definition/compiler_definition.h index e130759..b72ceac 100644 --- a/src/compiler/definition/compiler_definition.h +++ b/src/compiler/definition/compiler_definition.h @@ -6,17 +6,51 @@ #define DA_LIB_IMPLEMENTATION #include "thirdparty/da.h" +#include "thirdparty/error.h" +#include "thirdparty/hashmap.h" #include "frontend/ast.h" #include "middleend/hir.h" #include "middleend/ir_definition.h" typedef struct { - char* text; - parser_t parser; + char** items; + size_t count; + size_t capacity; +} compiled_files_array; + +typedef struct { + char* file_path; // not owned (points into files array) + char* module_name; // owned, built from DECLARATION_MODULE path + char* source; // owned + int source_len; + error_context_t error_ctx; + parser_t parser; declaration_array program; - IR_function_array* hir_program; + hashmap_t* export_funcs; +} module_unit_t; + +typedef struct { + module_unit_t** items; + size_t count; + size_t capacity; +} module_unit_array; + +typedef struct { + compiled_files_array files; + module_unit_array units; + IR_function_array* hir_program; + const char* output; } compiler_resources_t; +typedef struct { + hashmap_t* registry; + module_unit_t** items; // act as topo_order + size_t count; // must compile the files in the order of this array + size_t capacity; +} build_context_t; + +void build_context_free(build_context_t* ctx); +void module_unit_free(module_unit_t* unit); void compiler_resources_free(compiler_resources_t* res); #endif // COMPILER_DEFINITION_H diff --git a/src/compiler/setup/compiler_setup.c b/src/compiler/setup/compiler_setup.c new file mode 100644 index 0000000..c3317d1 --- /dev/null +++ b/src/compiler/setup/compiler_setup.c @@ -0,0 +1,63 @@ +#include "compiler_setup.h" + +compiler_resources_t* single_file_setup(int argc, char** argv) +{ + log_verbosity_t verbosity = LOG_VERBOSE; + const char* output = NULL; + char* filename = NULL; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-V") == 0) + verbosity = LOG_DUMP; + else if (strcmp(argv[i], "-o") == 0) { + if (++i >= argc) { + error_report_general( + ERROR_SEVERITY_ERROR, "missing argument for '-o'"); + return NULL; + } + output = argv[i]; + } + else if (argv[i][0] != '-') + filename = argv[i]; + else { + error_report_general( + ERROR_SEVERITY_ERROR, "unknown flag '%s'", argv[i]); + fprintf( + stderr, "usage: %s [-V] [-o ] \n", + argv[0]); + return NULL; + } + } + + log_set_verbosity(verbosity); + + if (!filename) { + error_report_general( + ERROR_SEVERITY_ERROR, "no input file provided"); + fprintf( + stderr, "usage: %s [-v|-V] [-o ] \n", + argv[0]); + return NULL; + } + + compiler_resources_t* res = + calloc(1, sizeof(compiler_resources_t)); + + res->output = output; + da_append(&(res->files), strdup(filename)); + return res; +} + +compiler_resources_t* build_setup() +{ + log_verbosity_t verbosity = LOG_DUMP; + log_set_verbosity(verbosity); + + compiler_resources_t* res = + calloc(1, sizeof(compiler_resources_t)); + + res->files = find_source_files(); + + return res; +} + diff --git a/src/compiler/setup/compiler_setup.h b/src/compiler/setup/compiler_setup.h new file mode 100644 index 0000000..47047b8 --- /dev/null +++ b/src/compiler/setup/compiler_setup.h @@ -0,0 +1,11 @@ +#ifndef COMPILER_SETUP_H +#define COMPILER_SETUP_H + +#include "compiler/definition/compiler_definition.h" +#include "thirdparty/log.h" +#include "compiler/build/file_scanner.h" + +compiler_resources_t* single_file_setup(int argc, char** argv); +compiler_resources_t* build_setup(); + +#endif // COMPILER_SETUP_H diff --git a/src/frontend/ast.c b/src/frontend/ast.c index 65321e9..4a5cafb 100644 --- a/src/frontend/ast.c +++ b/src/frontend/ast.c @@ -42,6 +42,12 @@ void free_expression(expression_t* e) free(e->call.args); } + + if (e->call.qualifier) + free(e->call.qualifier); + + if (e->call.resolved_module) + free(e->call.resolved_module); } if (e->type == EXPRESSION_UNARY) @@ -190,6 +196,25 @@ void free_declaration(declaration_t* d) if(d->var_decl.init) free_expression(d->var_decl.init); } + + if (d->type == DECLARATION_MODULE) { + da_foreach(char*, it, &(d->module.path)) { + free(*it); + } + da_free(&(d->module.path)); + } + + if (d->type == DECLARATION_IMPORT) { + da_foreach(char*, it, &(d->import.path)) { + free(*it); + } + + if (d->import.alias) { + free(d->import.alias); + } + + da_free(&(d->import.path)); + } free(d); } @@ -645,7 +670,7 @@ expression_t* ast_parse_expr_index(parser_t* p) expression_t* ast_parse_expr_call(parser_t* p) { - expression_t* e = (expression_t*) malloc(sizeof(expression_t)); + expression_t* e = calloc(1, sizeof(expression_t)); if (!e) { error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); return NULL; @@ -653,10 +678,32 @@ expression_t* ast_parse_expr_call(parser_t* p) e->type = EXPRESSION_CALL; e->source_pos = peek(p)->source_pos; + if (check_next(p, LEXER_token_coloncolon, 1)) { + token_t* qualifier_tok = advance(p); + if (!qualifier_tok->string_value) { + error_report_at_token( + p->error_ctx, qualifier_tok, ERROR_SEVERITY_ERROR, + "identifier has no value"); + free_expression(e); + return NULL; + } + + e->call.qualifier = strdup(qualifier_tok->string_value); + if (!e->call.qualifier) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free_expression(e); + return NULL; + } + + // consume '::' + advance(p); + } + token_t* name_tok = advance(p); if (!name_tok->string_value) { - error_report_at_token(p->error_ctx, name_tok, ERROR_SEVERITY_ERROR, - "identifier has no value"); + error_report_at_token( + p->error_ctx, name_tok, ERROR_SEVERITY_ERROR, + "identifier has no value"); free_expression(e); return NULL; } @@ -809,7 +856,9 @@ expression_t* parse_primary(parser_t* p) if (check(p, LEXER_token_id) && check_next(p, '[', 1)) return ast_parse_expr_index(p); - if (check(p, LEXER_token_id) && check_next(p, '(', 1)) + if (check(p, LEXER_token_id) && + (check_next(p, '(', 1) || + check_next(p, LEXER_token_coloncolon, 1))) return ast_parse_expr_call(p); if (check(p, LEXER_token_id)) @@ -894,7 +943,14 @@ declaration_t* ast_parse_function(parser_t* p) decl->type = DECLARATION_FUNC; decl->func.return_type = p->types->items[TYPE_UNTYPE]; decl->source_pos = peek(p)->source_pos; + decl->func.is_internal = false; + if (strcmp(peek(p)->string_value, "internal") == 0) { + decl->func.is_internal = true; + // consume 'internal' + advance(p); + } + // consume 'fn' advance(p); @@ -1277,6 +1333,128 @@ declaration_t* ast_parse_untype_var_decl(parser_t* p) return d; } +declaration_t* ast_parse_import_decl(parser_t* p) +{ + declaration_t* decl = calloc(1, sizeof(declaration_t)); + if (!decl) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return NULL; + } + decl->type = DECLARATION_IMPORT; + decl->source_pos = peek(p)->source_pos; + + // consume 'import' + advance(p); + + do { + if (!check(p, LEXER_token_id)) { + error_report_at_token( + p->error_ctx, peek(p), ERROR_SEVERITY_ERROR, + "expect identifier after `import`"); + free_declaration(decl); + return NULL; + } + + token_t* name_tok = advance(p); + if (!name_tok->string_value) { + error_report_at_token( + p->error_ctx, name_tok, ERROR_SEVERITY_ERROR, + "expect module name"); + free_declaration(decl); + return NULL; + } + + char* n = strdup(name_tok->string_value); + if (!n) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free_declaration(decl); + return NULL; + } + da_append(&decl->import.path, n); + + if (!check(p, LEXER_token_coloncolon)) + break; + + // consume '::' + advance(p); + } while (!check(p, LEXER_token_eof)); + + if (check(p, LEXER_token_id) && + strcmp(peek(p)->string_value, "as") == 0) { + // consume 'as' + advance(p); + + token_t* name_tok = advance(p); + if (!name_tok->string_value) { + error_report_at_token( + p->error_ctx, name_tok, ERROR_SEVERITY_ERROR, + "expect module name"); + free_declaration(decl); + return NULL; + } + + char* alias = strdup(name_tok->string_value); + if (!alias) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free_declaration(decl); + return NULL; + } + + decl->import.alias = alias; + } + + return decl; +} + +declaration_t* ast_parse_module_decl(parser_t* p) +{ + declaration_t* decl = calloc(1, sizeof(declaration_t)); + if (!decl) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + return NULL; + } + decl->type = DECLARATION_MODULE; + decl->source_pos = peek(p)->source_pos; + + // consume 'module' keyword + advance(p); + + do { + if (!check(p, LEXER_token_id)) { + error_report_at_token( + p->error_ctx, peek(p), ERROR_SEVERITY_ERROR, + "expect module name after `module` keyword"); + free_declaration(decl); + return NULL; + } + + token_t* name_tok = advance(p); + if (!name_tok->string_value) { + error_report_at_token( + p->error_ctx, name_tok, ERROR_SEVERITY_ERROR, + "expect module name"); + free_declaration(decl); + return NULL; + } + + char* m = strdup(name_tok->string_value); + if (!m) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + free_declaration(decl); + return NULL; + } + da_append(&(decl->module.path), m); + + if (!check(p, LEXER_token_coloncolon)) + break; + + // consume '::' + advance(p); + } while (!check(p, LEXER_token_eof)); + + return decl; +} + declaration_t* ast_parse_struct_decl(parser_t* p) { size_t total_struct_size = 0; @@ -1428,15 +1606,27 @@ declaration_t* ast_parse_struct_decl(parser_t* p) declaration_t* parse_declaration(parser_t* p) { - if (check(p, LEXER_token_id) && strcmp(peek(p)->string_value, "fn") == 0) { + if (check(p, LEXER_token_id) && + (strcmp(peek(p)->string_value, "fn") == 0 || + strcmp(peek(p)->string_value, "internal") == 0)) { return ast_parse_function(p); } - if (check(p, LEXER_token_id) && strcmp(peek(p)->string_value, - "struct") == 0) { + if (check(p, LEXER_token_id) && + strcmp(peek(p)->string_value, "struct") == 0) { return ast_parse_struct_decl(p); } + if (check(p, LEXER_token_id) && + strcmp(peek(p)->string_value, "module") == 0) { + return ast_parse_module_decl(p); + } + + if (check(p, LEXER_token_id) && + strcmp(peek(p)->string_value, "import") == 0) { + return ast_parse_import_decl(p); + } + if (check(p, LEXER_token_id) && check_is_type(p)) { return ast_parse_var_decl(p); } diff --git a/src/frontend/ast.h b/src/frontend/ast.h index 8f6f206..de47bfe 100644 --- a/src/frontend/ast.h +++ b/src/frontend/ast.h @@ -48,6 +48,8 @@ declaration_t* ast_parse_function(parser_t* p); declaration_t* ast_parse_var_decl(parser_t* p); declaration_t* ast_parse_untype_var_decl(parser_t* p); declaration_t* ast_parse_struct_decl(parser_t* p); +declaration_t* ast_parse_module_decl(parser_t* p); +declaration_t* ast_parse_import_decl(parser_t* p); declaration_t* parse_declaration(parser_t* p); statement_t* ast_parse_return_stmt(parser_t* p); diff --git a/src/frontend/ast_definition.h b/src/frontend/ast_definition.h index 362b6f8..f49413c 100644 --- a/src/frontend/ast_definition.h +++ b/src/frontend/ast_definition.h @@ -13,6 +13,8 @@ typedef enum DECLARATION_VAR, DECLARATION_FUNC, DECLARATION_STRUCT, + DECLARATION_MODULE, + DECLARATION_IMPORT, } declaration_kind; typedef enum @@ -123,6 +125,13 @@ typedef struct size_t capacity; } statement_block_t; +typedef struct +{ + char** items; + size_t count; + size_t capacity; +} import_path_t; + // ----------------- Declarations ------------------ struct declaration_t @@ -141,12 +150,22 @@ struct declaration_t known_type_t return_type; typed_identifier_array params; statement_block_t* body; + bool is_internal; } func; struct { char* name; typed_identifier_array members; } struc; + + struct { + import_path_t path; + } module; + + struct { + import_path_t path; + char* alias; + } import; }; }; @@ -211,9 +230,11 @@ struct expression_t binary_op_kind op; } binary; struct { + char* qualifier; char* callee; expression_t** args; size_t arg_count; + char* resolved_module; } call; struct { unary_op_kind op; diff --git a/src/frontend/ast_printer.c b/src/frontend/ast_printer.c index d3c482f..6c85cf3 100644 --- a/src/frontend/ast_printer.c +++ b/src/frontend/ast_printer.c @@ -132,8 +132,12 @@ static void print_expression(expression_t* e, const char* prefix, bool is_last) break; case EXPRESSION_CALL: - printf(CLR_STMT "CallExpr" CLR_RESET " '%s'\n", - e->call.callee ? e->call.callee : ""); + if (e->call.qualifier) + printf(CLR_STMT "CallExpr" CLR_RESET " '%s::%s'\n", + e->call.qualifier, e->call.callee ? e->call.callee : ""); + else + printf(CLR_STMT "CallExpr" CLR_RESET " '%s'\n", + e->call.callee ? e->call.callee : ""); for (size_t i = 0; i < e->call.arg_count; i++) print_expression(e->call.args[i], cp, i == e->call.arg_count - 1); break; @@ -297,7 +301,8 @@ static void print_declaration(declaration_t* d, const char* prefix, bool is_last switch (d->type) { case DECLARATION_FUNC: { - printf(CLR_DECL "FunctionDecl" CLR_RESET " '%s'(", + printf(CLR_DECL "%sFunctionDecl" CLR_RESET " '%s'(", + d->func.is_internal ? "internal " : "", d->func.name ? d->func.name : ""); for (size_t i = 0; i < d->func.params.count; i++) { @@ -354,6 +359,30 @@ static void print_declaration(declaration_t* d, const char* prefix, bool is_last } break; } + + case DECLARATION_MODULE: { + printf(CLR_DECL "ModuleDecl" CLR_RESET " "); + for (size_t i = 0; i < d->module.path.count; i++) { + printf("%s%s", + d->module.path.items[i] ? d->module.path.items[i] : "?", + i < d->module.path.count - 1 ? "::" : ""); + } + printf("\n"); + break; + } + + case DECLARATION_IMPORT: { + printf(CLR_DECL "ImportDecl" CLR_RESET " "); + for (size_t i = 0; i < d->import.path.count; i++) { + printf("%s%s", + d->import.path.items[i] ? d->import.path.items[i] : "?", + i < d->import.path.count - 1 ? "::" : ""); + } + if (d->import.alias) + printf(CLR_CONST " as %s" CLR_RESET, d->import.alias); + printf("\n"); + break; + } } } diff --git a/src/frontend/lexer.h b/src/frontend/lexer.h index 01a8385..a978ea8 100644 --- a/src/frontend/lexer.h +++ b/src/frontend/lexer.h @@ -41,6 +41,7 @@ There is still no copy past from it at all. Otherwise it would make no sence to #define LEXER_LIB_DOUBLE_ARROW Y // "=>" LEXER_token_darrow #define LEXER_LIB_LOGICAL Y // "||" LEXER_token_or // "&&" LEXER_token_and +#define LEXER_LIB_COLONCOLON Y // "::' LEXER_token_coloncolon #define LEXER_LIB_SQ_STRINGS N // single quotes delimited strings LEXER_token_sqstring #define LEXER_LIB_DQ_STRINGS Y // doubles quotes delimited string LEXER_token_dqstring #define LEXER_LIB_LIT_CHARS Y // single quotes delimited char with escape LEXER_token_charlit @@ -120,7 +121,8 @@ enum LEXER_token_and, LEXER_token_sqstring, LEXER_token_dqstring, - LEXER_token_charlit + LEXER_token_charlit, + LEXER_token_coloncolon }; // So we can #if on each token definition @@ -356,6 +358,9 @@ int lexer_get_token(lexer_t* l) } } goto single_char; + case ':': + LEXER_LIB_COLONCOLON( if (p+1 != l->eof && p[1] == ':') return lexer_create_token(l, LEXER_token_coloncolon, p+1);) + goto single_char; case '<': LEXER_LIB_COMPARISON( if (p+1 != l->eof && p[1] == '=') return lexer_create_token(l, LEXER_token_lseq, p+1);) goto single_char; @@ -449,6 +454,7 @@ static void lexer_print_token(lexer_t *l) case LEXER_token_dqstring: printf("\"%s\"", l->string_value); break; case LEXER_token_charlit: printf("'%c'", (unsigned char) l->int_value); break; case LEXER_token_id: printf("_%s", l->string_value); break; + case LEXER_token_coloncolon: printf("::"); break; default: if (l->token >= 0 && l->token < 256) printf("%c", (int) l->token); diff --git a/src/frontend/semantic.c b/src/frontend/semantic.c index 2a24aba..0dbd3e7 100644 --- a/src/frontend/semantic.c +++ b/src/frontend/semantic.c @@ -44,6 +44,19 @@ void semantic_free_program_definition(semantic_analyzer_t* analyzer) free(analyzer->function_symbols); } + if (analyzer->imported_functions) { + hashmap_free(analyzer->imported_functions, 0); + free(analyzer->imported_functions); + analyzer->imported_functions = NULL; + } + + da_foreach(imported_symbol_t*, it, &analyzer->imported_owned) { + free((*it)->module_name); + free((*it)->qualifier); + free(*it); + } + da_free(&analyzer->imported_owned); + if (analyzer->struct_symbols) { for (size_t i = 0; i < 211; ++i) { if (analyzer->struct_symbols->buckets[i]) { @@ -478,9 +491,49 @@ known_type_t semantic_check_expr_call( expression_t* expr, scope_t* scope) { - function_symbol_t* fs = (function_symbol_t*) hashmap_get( - analyzer->function_symbols, - expr->call.callee); + function_symbol_t* fs = NULL; + + if (expr->call.qualifier) { + imported_symbol_t* isym = NULL; + + if (analyzer->imported_functions) { + size_t key_len = + strlen(expr->call.qualifier) + 2 + strlen(expr->call.callee) + 1; + char* key = malloc(key_len); + if (key) { + snprintf(key, key_len, "%s::%s", + expr->call.qualifier, expr->call.callee); + isym = (imported_symbol_t*) hashmap_get( + analyzer->imported_functions, key); + free(key); + } + } + + if (!isym) { + semantic_error_register(analyzer, + expr->source_pos - 1, + "unknown qualified function call (module not imported or " + "qualifier does not match the source module)"); + return (known_type_t){.kind = TYPE_ERROR}; + } + + fs = isym->fs; + expr->call.resolved_module = strdup(isym->module_name); + } else { + fs = (function_symbol_t*) hashmap_get( + analyzer->function_symbols, + expr->call.callee); + + if (!fs && analyzer->imported_functions) { + imported_symbol_t* isym = (imported_symbol_t*) hashmap_get( + analyzer->imported_functions, expr->call.callee); + if (isym) { + fs = isym->fs; + expr->call.resolved_module = strdup(isym->module_name); + } + } + } + if (!fs) { semantic_error_register(analyzer, expr->source_pos - 1, diff --git a/src/frontend/semantic.h b/src/frontend/semantic.h index 19fcc8f..2db09ac 100644 --- a/src/frontend/semantic.h +++ b/src/frontend/semantic.h @@ -47,6 +47,13 @@ typedef struct size_t capacity; } diagnostics_t; +typedef struct +{ + imported_symbol_t** items; + size_t count; + size_t capacity; +} imported_symbol_array; + typedef struct { error_context_t* error_ctx; @@ -58,6 +65,9 @@ typedef struct hashmap_t* function_symbols; hashmap_t* struct_symbols; + hashmap_t* imported_functions; + imported_symbol_array imported_owned; + const char* current_analyzed_function; } semantic_analyzer_t; diff --git a/src/frontend/symbols.h b/src/frontend/symbols.h index a19abd5..3800c6a 100644 --- a/src/frontend/symbols.h +++ b/src/frontend/symbols.h @@ -25,4 +25,10 @@ typedef struct { size_t total_size; } struct_symbol_t; +typedef struct { + function_symbol_t* fs; + char* module_name; + char* qualifier; +} imported_symbol_t; + #endif // SYMBOLS_H diff --git a/src/middleend/hir.c b/src/middleend/hir.c index 94a83df..8db8655 100644 --- a/src/middleend/hir.c +++ b/src/middleend/hir.c @@ -3,6 +3,36 @@ static size_t min(size_t a, size_t b) { return a < b ? a : b; } + +char* IR_mangle_function_name(const char* module_path, const char* func_name) +{ + if (!module_path) + return strdup(func_name); + + if (strcmp(module_path, "main") == 0 && strcmp(func_name, "main") == 0) + return strdup("start"); + + char* mangled_module = strdup(module_path); + if (!mangled_module) return NULL; + + for (char* p = mangled_module; *p; ++p) { + if (p[0] == ':' && p[1] == ':') { + p[0] = '_'; + p[1] = '_'; + } + } + + size_t len = strlen(mangled_module) + 2 + strlen(func_name) + 1; + char* out = malloc(len); + if (!out) { + free(mangled_module); + return NULL; + } + snprintf(out, len, "%s__%s", mangled_module, func_name); + free(mangled_module); + + return out; +} void IR_free_instruction(IR_instruction_t* instr) { switch (instr->kind) { @@ -457,7 +487,10 @@ int IR_lower_call_expression( return 1; } call->kind = IR_CALL; - call->func_name = strdup(expr->call.callee); + const char* call_module = expr->call.resolved_module + ? expr->call.resolved_module + : hir->current_module; + call->func_name = IR_mangle_function_name(call_module, expr->call.callee); if (!call->func_name) { error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); return 1; @@ -1096,7 +1129,7 @@ int IR_lower_return_statement(HIR_parser_t* hir, return -1; } - if (strcmp(func->name, "main") == 0) { + if (strcmp(func->name, "main") == 0 || strcmp(func->name, "start") == 0) { instr->kind = IR_EXIT; instr->dest.id = func->next_temp_id; instr->dest.size = func->code->items[func->code->count - 1]->dest.size; @@ -1220,7 +1253,7 @@ int IR_lower_function(HIR_parser_t* hir, return -1; } - func->name = strdup(function->func.name); + func->name = IR_mangle_function_name(hir->current_module, function->func.name); if (!func->name) { error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); return -1; diff --git a/src/middleend/hir.h b/src/middleend/hir.h index e1d30bd..7fac493 100644 --- a/src/middleend/hir.h +++ b/src/middleend/hir.h @@ -27,10 +27,15 @@ typedef struct chunk_name_gen_t gen_chunk; void* chunk_ctx; + const char* current_module; + hashmap_t* struct_symbols; IR_function_array* hir_program; } HIR_parser_t; +char* IR_mangle_function_name( + const char* module_path, const char* func_name); + int IR_lower_function( HIR_parser_t* hir, declaration_t* function); diff --git a/test/ast_test.c b/test/ast_test.c index 4b8d19e..6853726 100644 --- a/test/ast_test.c +++ b/test/ast_test.c @@ -199,6 +199,7 @@ ct_test(ast, function_call, "test(a, 5);") ct_assert_eq(e->type, EXPRESSION_CALL, "Expression type should be CALL"); ct_assert_eq(e->call.callee, "test", "Function name should be 'test'"); + ct_assert_null(e->call.qualifier, "unqualified call should have NULL qualifier"); ct_assert_eq((int)e->call.arg_count, 2, "Function call should have 3 args"); ct_assert_eq(e->call.args[0]->var.ident.ident_name, "a", "Arg1 should be var 'a'"); @@ -208,6 +209,38 @@ ct_test(ast, function_call, "test(a, 5);") da_free(&parser); } +ct_test(ast, qualified_call_no_args, "io::print();") +{ + statement_t* s = parse_statement(&parser); + expression_t* e = s->expr_stmt.expr; + + ct_assert_not_null(e, "expression should not be NULL"); + ct_assert_eq(e->type, EXPRESSION_CALL, "should be a call expression"); + ct_assert_not_null(e->call.qualifier, "qualifier should not be NULL"); + ct_assert_eq(e->call.qualifier, "io", "qualifier should be 'io'"); + ct_assert_eq(e->call.callee, "print", "callee should be 'print'"); + ct_assert_eq((int)e->call.arg_count, 0, "should have 0 args"); + + free_statement(s); + da_free(&parser); +} + +ct_test(ast, qualified_call_with_args, "math::add(1, 2);") +{ + statement_t* s = parse_statement(&parser); + expression_t* e = s->expr_stmt.expr; + + ct_assert_not_null(e, "expression should not be NULL"); + ct_assert_eq(e->call.qualifier, "math", "qualifier should be 'math'"); + ct_assert_eq(e->call.callee, "add", "callee should be 'add'"); + ct_assert_eq((int)e->call.arg_count, 2, "should have 2 args"); + ct_assert_eq(e->call.args[0]->int_lit.value, 1, "first arg should be 1"); + ct_assert_eq(e->call.args[1]->int_lit.value, 2, "second arg should be 2"); + + free_statement(s); + da_free(&parser); +} + ct_test(ast, unary_pre_inc, "++i;") { statement_t* s = parse_statement(&parser); @@ -804,3 +837,168 @@ ct_test(ast, array_index_nonzero, "arr[3];") free_statement(s); da_free(&parser); } + +// === MODULE DECLARATION TESTS === + +ct_test(ast, module_basic, "module mymod") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "module decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_MODULE, "declaration type should be DECLARATION_MODULE"); + ct_assert_eq(decl->module.path.count, 1, "path should have 1 segment"); + ct_assert_eq(decl->module.path.items[0], "mymod", "module name should match source"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, module_nested, "module std::io") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "module decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_MODULE, "declaration type should be DECLARATION_MODULE"); + ct_assert_eq(decl->module.path.count, 2, "path should have 2 segments"); + ct_assert_eq(decl->module.path.items[0], "std", "first segment should be 'std'"); + ct_assert_eq(decl->module.path.items[1], "io", "second segment should be 'io'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, module_deep_nested, "module std::io::fs") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "module decl should not be NULL"); + ct_assert_eq(decl->module.path.count, 3, "path should have 3 segments"); + ct_assert_eq(decl->module.path.items[0], "std", "first segment should be 'std'"); + ct_assert_eq(decl->module.path.items[1], "io", "second segment should be 'io'"); + ct_assert_eq(decl->module.path.items[2], "fs", "third segment should be 'fs'"); + + free_declaration(decl); + da_free(&parser); +} + +// === IMPORT DECLARATION TESTS === + +ct_test(ast, import_simple, "import std::io") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_IMPORT, "declaration type should be DECLARATION_IMPORT"); + ct_assert_eq(decl->import.path.count, 2, "path should have 2 segments"); + ct_assert_eq(decl->import.path.items[0], "std", "first segment should be 'std'"); + ct_assert_eq(decl->import.path.items[1], "io", "second segment should be 'io'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, import_nested, "import std::io::print") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_IMPORT, "declaration type should be DECLARATION_IMPORT"); + ct_assert_eq(decl->import.path.count, 3, "path should have 3 segments"); + ct_assert_eq(decl->import.path.items[0], "std", "first segment should be 'std'"); + ct_assert_eq(decl->import.path.items[1], "io", "second segment should be 'io'"); + ct_assert_eq(decl->import.path.items[2], "print", "third segment should be 'print'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, import_single_segment, "import mymod") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_IMPORT, "declaration type should be DECLARATION_IMPORT"); + ct_assert_eq(decl->import.path.count, 1, "path should have 1 segment"); + ct_assert_eq(decl->import.path.items[0], "mymod", "segment should be 'mymod'"); + ct_assert_null(decl->import.alias, "no alias should be NULL"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, import_with_alias, "import std::io::print as p") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_IMPORT, "declaration type should be DECLARATION_IMPORT"); + ct_assert_eq(decl->import.path.count, 3, "path should have 3 segments"); + ct_assert_eq(decl->import.path.items[2], "print", "last segment should be 'print'"); + ct_assert_not_null(decl->import.alias, "alias should not be NULL"); + ct_assert_eq(decl->import.alias, "p", "alias should be 'p'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, import_alias_no_conflict, "import math::add as math_add") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_eq(decl->import.path.items[0], "math", "module should be 'math'"); + ct_assert_eq(decl->import.path.items[1], "add", "symbol should be 'add'"); + ct_assert_eq(decl->import.alias, "math_add", "alias should be 'math_add'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, import_no_alias, "import std::io::print") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "import decl should not be NULL"); + ct_assert_null(decl->import.alias, "import without alias should have NULL alias"); + + free_declaration(decl); + da_free(&parser); +} + +// === INTERNAL FUNCTION TESTS === + +ct_test(ast, internal_fn_basic, "internal fn secret(): int { return 0; }") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "decl should not be NULL"); + ct_assert_eq(decl->type, DECLARATION_FUNC, "should be a function declaration"); + ct_assert(decl->func.is_internal, "function should be marked internal"); + ct_assert_eq(decl->func.name, "secret", "function name should be 'secret'"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, internal_fn_with_params, "internal fn add(int a, int b): int { return 0; }") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "decl should not be NULL"); + ct_assert(decl->func.is_internal, "function should be marked internal"); + ct_assert_eq(decl->func.params.count, 2, "should have 2 params"); + + free_declaration(decl); + da_free(&parser); +} + +ct_test(ast, non_internal_fn, "fn foo(): int { return 0; }") +{ + declaration_t* decl = parse_declaration(&parser); + + ct_assert_not_null(decl, "decl should not be NULL"); + ct_assert(!decl->func.is_internal, "regular function should not be internal"); + + free_declaration(decl); + da_free(&parser); +} diff --git a/test/build_case/main_alias_call_ok.clf b/test/build_case/main_alias_call_ok.clf new file mode 100644 index 0000000..cf34846 --- /dev/null +++ b/test/build_case/main_alias_call_ok.clf @@ -0,0 +1,5 @@ +module main + +import math::add as madd + +internal fn main(): int { return madd(1, 2); } diff --git a/test/build_case/main_bare_call_ok.clf b/test/build_case/main_bare_call_ok.clf new file mode 100644 index 0000000..8f0916d --- /dev/null +++ b/test/build_case/main_bare_call_ok.clf @@ -0,0 +1,5 @@ +module main + +import math::add + +internal fn main(): int { return add(1, 2); } diff --git a/test/build_case/main_internal_violation.clf b/test/build_case/main_internal_violation.clf new file mode 100644 index 0000000..d831890 --- /dev/null +++ b/test/build_case/main_internal_violation.clf @@ -0,0 +1,5 @@ +module main + +import math::secret + +internal fn main(): int { return secret(); } diff --git a/test/build_case/main_missing_symbol.clf b/test/build_case/main_missing_symbol.clf new file mode 100644 index 0000000..def016d --- /dev/null +++ b/test/build_case/main_missing_symbol.clf @@ -0,0 +1,5 @@ +module main + +import math::missing + +internal fn main(): int { return missing(); } diff --git a/test/build_case/main_qualified_call_ok.clf b/test/build_case/main_qualified_call_ok.clf new file mode 100644 index 0000000..a5c74cb --- /dev/null +++ b/test/build_case/main_qualified_call_ok.clf @@ -0,0 +1,5 @@ +module main + +import math::add + +internal fn main(): int { return math::add(1, 2); } diff --git a/test/build_case/main_qualifier_mismatch.clf b/test/build_case/main_qualifier_mismatch.clf new file mode 100644 index 0000000..48172ac --- /dev/null +++ b/test/build_case/main_qualifier_mismatch.clf @@ -0,0 +1,5 @@ +module main + +import math::add + +internal fn main(): int { return wrongmod::add(1, 2); } diff --git a/test/build_case/main_unknown_module.clf b/test/build_case/main_unknown_module.clf new file mode 100644 index 0000000..d713471 --- /dev/null +++ b/test/build_case/main_unknown_module.clf @@ -0,0 +1,5 @@ +module main + +import unknown::add + +internal fn main(): int { return add(1, 2); } diff --git a/test/build_case/math_internal.clf b/test/build_case/math_internal.clf new file mode 100644 index 0000000..787e057 --- /dev/null +++ b/test/build_case/math_internal.clf @@ -0,0 +1,3 @@ +module math + +internal fn secret(): int { return 42; } diff --git a/test/build_case/math_ok.clf b/test/build_case/math_ok.clf new file mode 100644 index 0000000..40fadc7 --- /dev/null +++ b/test/build_case/math_ok.clf @@ -0,0 +1,3 @@ +module math + +fn add(int a, int b): int { return a + b; } diff --git a/test/build_test.c b/test/build_test.c new file mode 100644 index 0000000..28ad5b7 --- /dev/null +++ b/test/build_test.c @@ -0,0 +1,168 @@ +#define CTEST_BEFORE_EACH +#define CTEST_LIB_IMPLEMENTATION +#include "ctest.h" + +#define LEXER_LIB_IMPLEMENTATION +#include "../src/frontend/lexer.h" +#define DA_LIB_IMPLEMENTATION +#include "../src/thirdparty/da.h" + +#include "../src/frontend/ast_definition.h" +#include "../src/frontend/ast.h" +#include "../src/frontend/semantic.h" +#include "../src/thirdparty/error.h" +#include "../src/compiler/definition/compiler_definition.h" +#include "../src/compiler/build/registry.h" +#include "../src/compiler/build/export_table.h" +#include "../src/compiler/build/import_resolver.h" + +// Loads and parses a single .clf file into a fresh module_unit_t. `path` +// must outlive the returned unit (it is not duplicated, mirroring how +// module_unit_t.file_path is a borrowed pointer in the real compiler). +static module_unit_t* load_module_unit(const char* path) +{ + FILE* f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "error opening test file '%s'\n", path); + abort(); + } + + module_unit_t* unit = calloc(1, sizeof(module_unit_t)); + if (!unit) abort(); + + unit->file_path = (char*) path; + unit->source = malloc(1 << 20); + unit->source_len = (int) fread(unit->source, 1, 1 << 20, f); + fclose(f); + + error_init(&unit->error_ctx, path, unit->source, unit->source_len); + unit->parser.error_ctx = &unit->error_ctx; + + lexer_t lex; + lexer_init_lexer( + &lex, unit->source, unit->source + unit->source_len, + malloc(4096), 4096); + + while (lexer_get_token(&lex)) { + if (lex.token == LEXER_token_parse_error) break; + token_t t = lexer_copy_token(&lex); + da_append(&unit->parser, t); + } + free(lex.string_storage); + + unit->parser.types = calloc(1, sizeof(known_type_array)); + populate_parser_known_type(unit->parser.types); + + while ((size_t) unit->parser.pos < unit->parser.count) { + declaration_t* decl = parse_declaration(&unit->parser); + if (!decl) break; + da_append(&unit->program, decl); + } + + return unit; +} + +typedef struct { + build_context_t ctx; + module_unit_t* dep_unit; + module_unit_t* main_unit; + semantic_analyzer_t analyzer; +} build_test_ctx_t; + +before_each(build_test_ctx_t, tctx, char* dep_path, char* main_path) +{ + build_test_ctx_t t = {0}; + + t.ctx.registry = calloc(1, sizeof(hashmap_t)); + if (!t.ctx.registry) abort(); + + t.dep_unit = load_module_unit(dep_path); + t.main_unit = load_module_unit(main_path); + + if (!populate_module_registry(&t.ctx, t.dep_unit)) abort(); + if (!populate_module_registry(&t.ctx, t.main_unit)) abort(); + + if (!semantic_build_export_table(t.dep_unit)) abort(); + if (!semantic_build_export_table(t.main_unit)) abort(); + + t.analyzer.error_ctx = &t.main_unit->error_ctx; + t.analyzer.ast = &t.main_unit->program; + + semantic_resolve_imports(&t.ctx, t.main_unit, &t.analyzer); + semantic_analyze(&t.analyzer); + + tctx = t; +} + +static void free_build_test_ctx(build_test_ctx_t* t) +{ + semantic_free_program_definition(&t->analyzer); + module_unit_free(t->dep_unit); + module_unit_free(t->main_unit); + build_context_free(&t->ctx); +} + +ct_test(build_import, bare_call_resolves, + "test/build_case/math_ok.clf", + "test/build_case/main_bare_call_ok.clf") +{ + ct_assert_eq(tctx.analyzer.error_count, 0, + "importing and calling a plain function should not error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, qualified_call_resolves, + "test/build_case/math_ok.clf", + "test/build_case/main_qualified_call_ok.clf") +{ + ct_assert_eq(tctx.analyzer.error_count, 0, + "calling an imported function through its module qualifier " + "should not error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, alias_call_resolves, + "test/build_case/math_ok.clf", + "test/build_case/main_alias_call_ok.clf") +{ + ct_assert_eq(tctx.analyzer.error_count, 0, + "calling an imported function through its alias should not error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, unknown_module_errors, + "test/build_case/math_ok.clf", + "test/build_case/main_unknown_module.clf") +{ + ct_assert((tctx.analyzer.error_count > 0), + "importing from an unregistered module should error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, missing_symbol_errors, + "test/build_case/math_ok.clf", + "test/build_case/main_missing_symbol.clf") +{ + ct_assert((tctx.analyzer.error_count > 0), + "importing an undefined symbol should error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, internal_violation_errors, + "test/build_case/math_internal.clf", + "test/build_case/main_internal_violation.clf") +{ + ct_assert((tctx.analyzer.error_count > 0), + "importing an internal function from another module should error"); + free_build_test_ctx(&tctx); +} + +ct_test(build_import, qualifier_mismatch_errors, + "test/build_case/math_ok.clf", + "test/build_case/main_qualifier_mismatch.clf") +{ + ct_assert((tctx.analyzer.error_count > 0), + "calling with a qualifier that doesn't match the imported " + "module should error"); + free_build_test_ctx(&tctx); +} diff --git a/test/hir_module_case/entry_point_main.clf b/test/hir_module_case/entry_point_main.clf new file mode 100644 index 0000000..17f5b90 --- /dev/null +++ b/test/hir_module_case/entry_point_main.clf @@ -0,0 +1,5 @@ +module main + +fn main(): int { + return 0; +} diff --git a/test/hir_module_case/entry_point_main.res b/test/hir_module_case/entry_point_main.res new file mode 100644 index 0000000..c5d91c0 --- /dev/null +++ b/test/hir_module_case/entry_point_main.res @@ -0,0 +1,3 @@ +Function start +0: t1 = INT_CONST 0 +1: EXIT t1 diff --git a/test/hir_module_case/local_call_math.clf b/test/hir_module_case/local_call_math.clf new file mode 100644 index 0000000..11cc8eb --- /dev/null +++ b/test/hir_module_case/local_call_math.clf @@ -0,0 +1,9 @@ +module math + +fn helper(): int { + return 1; +} + +fn add(): int { + return helper(); +} diff --git a/test/hir_module_case/local_call_math.res b/test/hir_module_case/local_call_math.res new file mode 100644 index 0000000..823ea5d --- /dev/null +++ b/test/hir_module_case/local_call_math.res @@ -0,0 +1,9 @@ +Function math__helper +0: t1 = INT_CONST 1 +1: MOV t-1 t1 +2: RETURN +Function math__add +0: CALL math__helper +1: MOV t1 t-1 +2: MOV t-1 t1 +3: RETURN diff --git a/test/hir_module_case/non_main_fn_main_module.clf b/test/hir_module_case/non_main_fn_main_module.clf new file mode 100644 index 0000000..c6f758d --- /dev/null +++ b/test/hir_module_case/non_main_fn_main_module.clf @@ -0,0 +1,5 @@ +module main + +fn foo(): int { + return 0; +} diff --git a/test/hir_module_case/non_main_fn_main_module.res b/test/hir_module_case/non_main_fn_main_module.res new file mode 100644 index 0000000..428e20a --- /dev/null +++ b/test/hir_module_case/non_main_fn_main_module.res @@ -0,0 +1,4 @@ +Function main__foo +0: t1 = INT_CONST 0 +1: MOV t-1 t1 +2: RETURN diff --git a/test/hir_module_test.c b/test/hir_module_test.c new file mode 100644 index 0000000..fc060aa --- /dev/null +++ b/test/hir_module_test.c @@ -0,0 +1,176 @@ +#define CTEST_BEFORE_EACH +#define CTEST_LIB_IMPLEMENTATION +#include "ctest.h" + +#define LEXER_LIB_IMPLEMENTATION +#include "../src/frontend/lexer.h" +#define DA_LIB_IMPLEMENTATION +#include "../src/thirdparty/da.h" + +#include "../src/frontend/ast_definition.h" +#include "../src/frontend/ast.h" +#include "../src/frontend/semantic.h" +#include "../src/middleend/hir.h" +#include "../src/thirdparty/error.h" + +// Same lex/parse/HIR pipeline as hir_test.c, but additionally sets +// `hir_parser.current_module` so we can assert on the Phase 3 name +// mangling behavior (module labels, mangled CALL targets, "_start" +// entry point detection) without needing a full multi-module build. +before_each(int, result, char* file_path, char* expected_path, char* module_name) +{ + FILE *f = fopen(file_path, "rb"); + if (f == NULL) { + fprintf(stderr, "error on test file %s\n", file_path); + abort(); + } + + char* text = (char*) malloc(1 << 20); + int len = f ? (int) fread(text, 1, 1<<20, f) : -1; + + if (len < 0) { + fprintf(stderr, "error while reading %s\n", file_path); + free(text); + fclose(f); + abort(); + } + fclose(f); + + parser_t p = {0}; + lexer_t lex; + + error_context_t* error_ctx = calloc(1, sizeof(error_context_t)); + if (!error_ctx) abort(); + error_init(error_ctx, file_path, text, len); + + declaration_array* program = calloc(1, sizeof(declaration_array)); + if (!program) abort(); + + char* storage = malloc(255); + if (!storage) abort(); + lexer_init_lexer(&lex, + text, + text + len, + storage, + 255); + + while (lexer_get_token(&lex)) { + if (lex.token == LEXER_token_parse_error) + break; + + token_t t = lexer_copy_token(&lex); + da_append(&p, t); + } + + free(storage); + + p.types = calloc(1, sizeof(known_type_array)); + populate_parser_known_type(p.types); + + while ((size_t)p.pos < p.count) { + declaration_t* decl = parse_declaration(&p); + da_append(program, decl); + } + + free(text); + + for (size_t i = 0; i < p.count; i++) { + if (p.items[i].string_value) { + free(p.items[i].string_value); + } + } + da_free(&p); + + IR_function_array* hir_program = calloc(1, sizeof(IR_function_array)); + if (!hir_program) { + error_report_general(ERROR_SEVERITY_ERROR, "out of memory"); + abort(); + } + + semantic_analyzer_t analyzer = {0}; + analyzer.error_ctx = error_ctx; + analyzer.ast = program; + analyzer.error_count = 0; + semantic_analyze(&analyzer); + + HIR_parser_t hir_parser = {0}; + hir_parser.error_ctx = error_ctx; + hir_parser.error_count = 0; + hir_parser.hir_program = hir_program; + hir_parser.struct_symbols = analyzer.struct_symbols; + hir_parser.current_module = module_name; + da_foreach(declaration_t*, it, program) { + int lowering_result = IR_lower_function(&hir_parser, *it); + if (lowering_result != 0) { + error_report_general(ERROR_SEVERITY_ERROR, + "hir parsing error"); + abort(); + } + } + + char output[2048] = "\0"; + da_foreach(IR_function_t*, it, hir_parser.hir_program) { + char* res = IR_generate_string_program(*it); + strcat(output, res); + free(res); + } + + semantic_free_program_definition(&analyzer); + + da_foreach(declaration_t*, it, program) { + free_declaration(*it); + } + da_free(program); + + FILE *fr = fopen(expected_path, "rb"); + if (fr == NULL) { + fprintf(stderr, "error on test file %s\n", expected_path); + abort(); + } + + char* textr = (char*) malloc(1 << 20); + int lenr = fr ? (int) fread(textr, 1, 1<<20, fr) : -1; + + if (lenr < 0) { + fprintf(stderr, "error while reading %s\n", expected_path); + free(textr); + fclose(fr); + abort(); + } + fclose(fr); + textr[lenr] = '\0'; + + result = strcmp(textr, output); + + free(textr); +} + +ct_test(hir_module_test, entry_point_main, + "test/hir_module_case/entry_point_main.clf", + "test/hir_module_case/entry_point_main.res", + "main") +{ + ct_assert_eq(result, 0, + "module 'main' function 'main' should be mangled to 'start' and " + "use the EXIT instruction"); +} + +ct_test(hir_module_test, local_call_math, + "test/hir_module_case/local_call_math.clf", + "test/hir_module_case/local_call_math.res", + "math") +{ + ct_assert_eq(result, 0, + "local calls within a module should be mangled with the module's " + "own name (module__symbol)"); +} + +ct_test(hir_module_test, non_main_fn_main_module, + "test/hir_module_case/non_main_fn_main_module.clf", + "test/hir_module_case/non_main_fn_main_module.res", + "main") +{ + ct_assert_eq(result, 0, + "a non-'main' function declared in module 'main' should be " + "mangled to 'main__', not treated as the entry point"); +} diff --git a/test/integration_case/cycle/a.clf b/test/integration_case/cycle/a.clf new file mode 100644 index 0000000..6a6598e --- /dev/null +++ b/test/integration_case/cycle/a.clf @@ -0,0 +1,7 @@ +module a + +import main::helper + +fn ping(): int { + return helper(); +} diff --git a/test/integration_case/cycle/expect.txt b/test/integration_case/cycle/expect.txt new file mode 100644 index 0000000..a6972b6 --- /dev/null +++ b/test/integration_case/cycle/expect.txt @@ -0,0 +1 @@ +build_exit=1 diff --git a/test/integration_case/cycle/main.clf b/test/integration_case/cycle/main.clf new file mode 100644 index 0000000..359308f --- /dev/null +++ b/test/integration_case/cycle/main.clf @@ -0,0 +1,11 @@ +module main + +import a::ping + +fn helper(): int { + return 1; +} + +internal fn main(): int { + return ping(); +} diff --git a/test/integration_case/internal_violation/expect.txt b/test/integration_case/internal_violation/expect.txt new file mode 100644 index 0000000..a6972b6 --- /dev/null +++ b/test/integration_case/internal_violation/expect.txt @@ -0,0 +1 @@ +build_exit=1 diff --git a/test/integration_case/internal_violation/main.clf b/test/integration_case/internal_violation/main.clf new file mode 100644 index 0000000..d080658 --- /dev/null +++ b/test/integration_case/internal_violation/main.clf @@ -0,0 +1,7 @@ +module main + +import math::helper + +internal fn main(): int { + return helper(); +} diff --git a/test/integration_case/internal_violation/math.clf b/test/integration_case/internal_violation/math.clf new file mode 100644 index 0000000..c88bb70 --- /dev/null +++ b/test/integration_case/internal_violation/math.clf @@ -0,0 +1,9 @@ +module math + +internal fn helper(): int { + return 41; +} + +fn add(): int { + return helper(); +} diff --git a/test/integration_case/missing_main/expect.txt b/test/integration_case/missing_main/expect.txt new file mode 100644 index 0000000..a6972b6 --- /dev/null +++ b/test/integration_case/missing_main/expect.txt @@ -0,0 +1 @@ +build_exit=1 diff --git a/test/integration_case/missing_main/math.clf b/test/integration_case/missing_main/math.clf new file mode 100644 index 0000000..8f0f2ec --- /dev/null +++ b/test/integration_case/missing_main/math.clf @@ -0,0 +1,5 @@ +module math + +fn add(): int { + return 41; +} diff --git a/test/integration_case/return_value_chain/expect.txt b/test/integration_case/return_value_chain/expect.txt new file mode 100644 index 0000000..4196b6b --- /dev/null +++ b/test/integration_case/return_value_chain/expect.txt @@ -0,0 +1,2 @@ +build_exit=0 +run_exit=41 diff --git a/test/integration_case/return_value_chain/main.clf b/test/integration_case/return_value_chain/main.clf new file mode 100644 index 0000000..0f4dd16 --- /dev/null +++ b/test/integration_case/return_value_chain/main.clf @@ -0,0 +1,7 @@ +module main + +import math::add + +internal fn main(): int { + return add(); +} diff --git a/test/integration_case/return_value_chain/math.clf b/test/integration_case/return_value_chain/math.clf new file mode 100644 index 0000000..c88bb70 --- /dev/null +++ b/test/integration_case/return_value_chain/math.clf @@ -0,0 +1,9 @@ +module math + +internal fn helper(): int { + return 41; +} + +fn add(): int { + return helper(); +} diff --git a/test/integration_test.sh b/test/integration_test.sh new file mode 100755 index 0000000..0106cc0 --- /dev/null +++ b/test/integration_test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Integration tests for `cleaf build`: each subdirectory of +# test/integration_case/ is a standalone multi-module project. An +# `expect.txt` file (key=value, one per line) declares the expected +# outcome: +# +# build_exit= required — expected exit code of `cleaf build` +# run_exit= optional — if set, the produced build/a.out is +# executed afterwards and its exit code checked +# +# Usage: test/integration_test.sh + +set -u + +CLEAF_BIN="${1:-build/cleaf}" +CLEAF_BIN="$(cd "$(dirname "$CLEAF_BIN")" && pwd)/$(basename "$CLEAF_BIN")" + +CASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/integration_case" && pwd)" + +fail=0 +total=0 + +for dir in "$CASE_DIR"/*/; do + name="$(basename "$dir")" + expect_file="$dir/expect.txt" + + if [ ! -f "$expect_file" ]; then + continue + fi + + total=$((total + 1)) + + expected_build_exit=$(grep '^build_exit=' "$expect_file" | cut -d= -f2) + expected_run_exit=$(grep '^run_exit=' "$expect_file" | cut -d= -f2) + + ( + cd "$dir" || exit 1 + rm -rf build a.out + "$CLEAF_BIN" build > /tmp/cleaf_integration_${name}.log 2>&1 + ) + actual_build_exit=$? + + if [ "$actual_build_exit" != "$expected_build_exit" ]; then + echo "[FAIL] $name: expected build exit $expected_build_exit, got $actual_build_exit" + echo " see /tmp/cleaf_integration_${name}.log" + fail=$((fail + 1)) + continue + fi + + if [ -n "$expected_run_exit" ]; then + (cd "$dir" && ./build/a.out) + actual_run_exit=$? + if [ "$actual_run_exit" != "$expected_run_exit" ]; then + echo "[FAIL] $name: expected run exit $expected_run_exit, got $actual_run_exit" + fail=$((fail + 1)) + continue + fi + fi + + echo "[ OK ] $name" +done + +echo "" +echo "$((total - fail))/$total integration test(s) passed" + +exit $([ "$fail" -eq 0 ] && echo 0 || echo 1) diff --git a/test/valgrind_case/combined_multiple_errors.clf b/test/valgrind_case/combined_multiple_errors.clf index 994e70a..37c351e 100644 --- a/test/valgrind_case/combined_multiple_errors.clf +++ b/test/valgrind_case/combined_multiple_errors.clf @@ -1,3 +1,5 @@ +module main + fn test(int a, int a): string { var b = undefined_var + 5; int c = "string" + 10; diff --git a/test/valgrind_case/full.clf b/test/valgrind_case/full.clf index 233ade2..bdb50d9 100644 --- a/test/valgrind_case/full.clf +++ b/test/valgrind_case/full.clf @@ -1,3 +1,5 @@ +module main + struct v2 { int x; int y; @@ -42,8 +44,7 @@ fn main(): int { return 0; } - -fn foo(): int { +internal fn foo(): int { var b = 12; return b; } diff --git a/test/valgrind_case/lexer_error.clf b/test/valgrind_case/lexer_error.clf index 184791d..d133742 100644 --- a/test/valgrind_case/lexer_error.clf +++ b/test/valgrind_case/lexer_error.clf @@ -1,3 +1,5 @@ +module main + fn main() { var a = @invalid; } diff --git a/test/valgrind_case/parser_for_missing_increment.clf b/test/valgrind_case/parser_for_missing_increment.clf index 4b771aa..286aace 100644 --- a/test/valgrind_case/parser_for_missing_increment.clf +++ b/test/valgrind_case/parser_for_missing_increment.clf @@ -1,3 +1,5 @@ +module main + fn main() { for (var i = 0; i < 10) { i++; diff --git a/test/valgrind_case/parser_incomplete_function.clf b/test/valgrind_case/parser_incomplete_function.clf index c12a976..1c76cac 100644 --- a/test/valgrind_case/parser_incomplete_function.clf +++ b/test/valgrind_case/parser_incomplete_function.clf @@ -1 +1,3 @@ +module main + fn main( diff --git a/test/valgrind_case/parser_missing_function_name.clf b/test/valgrind_case/parser_missing_function_name.clf index 75a916e..3363793 100644 --- a/test/valgrind_case/parser_missing_function_name.clf +++ b/test/valgrind_case/parser_missing_function_name.clf @@ -1,3 +1,5 @@ +module main + fn { return 0; } diff --git a/test/valgrind_case/parser_missing_paren.clf b/test/valgrind_case/parser_missing_paren.clf index 4454218..82cd11f 100644 --- a/test/valgrind_case/parser_missing_paren.clf +++ b/test/valgrind_case/parser_missing_paren.clf @@ -1,3 +1,5 @@ +module main + fn main() { var a = (5 + 3; } diff --git a/test/valgrind_case/parser_missing_semicolon.clf b/test/valgrind_case/parser_missing_semicolon.clf index 2ff69c5..80cd30b 100644 --- a/test/valgrind_case/parser_missing_semicolon.clf +++ b/test/valgrind_case/parser_missing_semicolon.clf @@ -1,3 +1,5 @@ +module main + fn main() { var a = 5 var b = 10; diff --git a/test/valgrind_case/parser_missing_var_name.clf b/test/valgrind_case/parser_missing_var_name.clf index d486f58..de90975 100644 --- a/test/valgrind_case/parser_missing_var_name.clf +++ b/test/valgrind_case/parser_missing_var_name.clf @@ -1,3 +1,5 @@ +module main + fn main() { var = 5; } diff --git a/test/valgrind_case/parser_unmatched_brace.clf b/test/valgrind_case/parser_unmatched_brace.clf index c9015f5..04cdb61 100644 --- a/test/valgrind_case/parser_unmatched_brace.clf +++ b/test/valgrind_case/parser_unmatched_brace.clf @@ -1,3 +1,5 @@ +module main + fn main() { var a = 5; if (a > 0) { diff --git a/test/valgrind_case/semantic_control_flow_errors.clf b/test/valgrind_case/semantic_control_flow_errors.clf index f874508..990371e 100644 --- a/test/valgrind_case/semantic_control_flow_errors.clf +++ b/test/valgrind_case/semantic_control_flow_errors.clf @@ -1,3 +1,5 @@ +module main + fn main() { int a = 5; for (var i = 0; undefined_cond < 10; ++i) { diff --git a/test/valgrind_case/semantic_function_args_error.clf b/test/valgrind_case/semantic_function_args_error.clf index e19c103..40c5acd 100644 --- a/test/valgrind_case/semantic_function_args_error.clf +++ b/test/valgrind_case/semantic_function_args_error.clf @@ -1,3 +1,5 @@ +module main + fn add(int a, int b): int { return a + b; } diff --git a/test/valgrind_case/semantic_function_duplicate.clf b/test/valgrind_case/semantic_function_duplicate.clf index 21fa962..93f7016 100644 --- a/test/valgrind_case/semantic_function_duplicate.clf +++ b/test/valgrind_case/semantic_function_duplicate.clf @@ -1,3 +1,5 @@ +module main + fn foo() { return 0; } diff --git a/test/valgrind_case/semantic_function_reserved.clf b/test/valgrind_case/semantic_function_reserved.clf index 74b1563..21cc3f0 100644 --- a/test/valgrind_case/semantic_function_reserved.clf +++ b/test/valgrind_case/semantic_function_reserved.clf @@ -1,3 +1,5 @@ +module main + fn var(): { return 0; } diff --git a/test/valgrind_case/semantic_return_type_error.clf b/test/valgrind_case/semantic_return_type_error.clf index 52de8ad..5acaa45 100644 --- a/test/valgrind_case/semantic_return_type_error.clf +++ b/test/valgrind_case/semantic_return_type_error.clf @@ -1,3 +1,5 @@ +module main + fn test(): string { return 42; } diff --git a/test/valgrind_case/semantic_type_mismatch.clf b/test/valgrind_case/semantic_type_mismatch.clf index 3248969..1ec6000 100644 --- a/test/valgrind_case/semantic_type_mismatch.clf +++ b/test/valgrind_case/semantic_type_mismatch.clf @@ -1,3 +1,5 @@ +module main + fn main() { int a = 5; string b = "test"; diff --git a/test/valgrind_case/semantic_unary_type_error.clf b/test/valgrind_case/semantic_unary_type_error.clf index f39cd7f..7a92920 100644 --- a/test/valgrind_case/semantic_unary_type_error.clf +++ b/test/valgrind_case/semantic_unary_type_error.clf @@ -1,3 +1,5 @@ +module main + fn main() { var a = -"string"; int b = !"test"; diff --git a/test/valgrind_case/semantic_undefined_function.clf b/test/valgrind_case/semantic_undefined_function.clf index e6f7c91..076a75b 100644 --- a/test/valgrind_case/semantic_undefined_function.clf +++ b/test/valgrind_case/semantic_undefined_function.clf @@ -1,3 +1,5 @@ +module main + fn main() { var result = undefined_function(); another_missing(5, 10); diff --git a/test/valgrind_case/semantic_undefined_vars.clf b/test/valgrind_case/semantic_undefined_vars.clf index 070f8d3..490c421 100644 --- a/test/valgrind_case/semantic_undefined_vars.clf +++ b/test/valgrind_case/semantic_undefined_vars.clf @@ -1,3 +1,5 @@ +module main + fn main() { int a = undefined_var + 5; var b = another_undef; diff --git a/test/valgrind_case/semantic_var_redefinition.clf b/test/valgrind_case/semantic_var_redefinition.clf index 5558358..fe31aa5 100644 --- a/test/valgrind_case/semantic_var_redefinition.clf +++ b/test/valgrind_case/semantic_var_redefinition.clf @@ -1,3 +1,5 @@ +module main + fn main() { int a = 5; int a = 10;