diff --git a/.gitignore b/.gitignore index 15c8f85..a669392 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ build/ .DS_Store deps/ design/ +.vscode +.claude \ No newline at end of file diff --git a/Makefile b/Makefile index 0d63c31..a722085 100644 --- a/Makefile +++ b/Makefile @@ -63,9 +63,9 @@ test: @passed=0; failed=0; errors=""; \ for f in tests/test_*.lv; do \ name=$$(basename $$f .lv); \ - /tmp/lavina_next --emit-cpp $$f > /tmp/$$name.cpp 2>/dev/null && \ - g++ -std=c++23 -I/tmp -o /tmp/$$name /tmp/$$name.cpp 2>/dev/null && \ - /tmp/$$name 2>/dev/null; \ + dir=$$(dirname $$f); \ + /tmp/lavina_next compile $$f 2>/dev/null && \ + $$dir/$$name 2>/dev/null; \ if [ $$? -eq 0 ]; then \ echo " PASS $$name"; \ passed=$$((passed + 1)); \ @@ -74,6 +74,7 @@ test: failed=$$((failed + 1)); \ errors="$$errors $$name"; \ fi; \ + rm -f $$dir/$$name $$dir/$$name.cpp; \ done; \ echo ""; \ echo "$$passed passed, $$failed failed"; \ diff --git a/README.md b/README.md index 86f3a5a..8816413 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ void fn main(): - **Block lambdas** — `(params):` with indented body - **Module system** — `import a::b`, `public`/`private`, `as` aliases - **Compile-time evaluation** — `comptime` / `comptime!` -- **FFI** — `extern` blocks, `cpp {}` inline C++, expanded types (`int8`/`int16`/`int32`, `float32`, `usize`, `cstring`, `ptr[T]`) +- **FFI** — `extern` blocks with `import`/`link` for automatic `-I`/`-l` flags, `cpp {}` inline C++, expanded types (`int8`/`int16`/`int32`, `float32`, `usize`, `cstring`, `ptr[T]`) - **Package manager** — `lvpkg` for dependency management - **Self-hosting** — the compiler bootstraps from a saved C++ snapshot @@ -83,6 +83,7 @@ design/ language design documents - `examples/complex/tree/` — file tree printer - `examples/complex/lvg/` — Git client - `examples/complex/webserver/` — HTTP todo app (uses httplib via lvpkg) +- `examples/complex/raylib/` — Raylib window with animated graphics - `examples/sqlite.lv` — SQLite via FFI ## Package Manager @@ -101,6 +102,9 @@ dep httplib https://github.com/yhirose/cpp-httplib v0.18.3 httplib.h # directory of headers dep json https://github.com/nlohmann/json v3.11.3 single_include/nlohmann/ + +# native library (pre-built binary + headers) +dep raylib https://github.com/raysan5/raylib 5.5 src/raylib.h ``` Then run: @@ -112,7 +116,7 @@ lvpkg list # show dependency status lvpkg clean # remove deps/ ``` -Dependencies are cloned into `deps/` and checked out at the pinned version. +Dependencies are cloned into `deps/` and checked out at the pinned version. Headers go to `deps/include/`, libraries to `deps/lib/`. The compiler automatically adds `-Ideps/include` and `-Ldeps/lib` when these directories exist. ## Documentation diff --git a/examples/complex/raylib/lavina.pkg b/examples/complex/raylib/lavina.pkg new file mode 100644 index 0000000..4195305 --- /dev/null +++ b/examples/complex/raylib/lavina.pkg @@ -0,0 +1,8 @@ +# Raylib example +# +# Headers are pulled from GitHub by lvpkg. +# The native library must be installed separately: +# macOS: brew install raylib +# Linux: sudo apt install libraylib-dev + +dep raylib https://github.com/raysan5/raylib 5.5 src/raylib.h diff --git a/examples/complex/raylib/raylavina.lv b/examples/complex/raylib/raylavina.lv new file mode 100644 index 0000000..1122a6d --- /dev/null +++ b/examples/complex/raylib/raylavina.lv @@ -0,0 +1,71 @@ +// Raylib example — basic window with animated circle +// +// Requires: raylib native library +// macOS: brew install raylib +// Linux: sudo apt install libraylib-dev +// +// Headers can be pulled via lvpkg (see lavina.pkg): +// lvpkg install +// +// Build: +// lavina compile raylavina.lv +// +// Note: import/link paths below are for Homebrew on macOS (arm64). +// Adjust paths for your platform if needed. + +extern "raylib.h" import "/opt/homebrew/opt/raylib/include" link "/opt/homebrew/opt/raylib/lib/libraylib.dylib": + type Color + type Vector2 + + void fn InitWindow(int32 width, int32 height, cstring title) + void fn CloseWindow() + bool fn WindowShouldClose() + void fn BeginDrawing() + void fn EndDrawing() + void fn ClearBackground(Color color) + void fn DrawCircle(int32 x, int32 y, float32 radius, Color color) + void fn DrawText(cstring text, int32 x, int32 y, int32 fontSize, Color color) + void fn SetTargetFPS(int32 fps) + int32 fn GetScreenWidth() + int32 fn GetScreenHeight() + float fn GetTime() = "GetTime" + +extern "cmath": + float fn sin_f(float x) = "std::sin" + float fn cos_f(float x) = "std::cos" + int fn lrint_f(float x) = "std::lrint" + +// Lavina does not yet support extern struct literals, so we init Color via cpp helper +cpp { + Color make_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + Color c = {r, g, b, a}; + return c; + } +} + +extern "raylib.h": + Color fn make_color(int32 r, int32 g, int32 b, int32 a) = "make_color" + +void fn main(): + int32 width = 800 + int32 height = 450 + + InitWindow(width, height, "Lavina + Raylib = <3") + SetTargetFPS(60) + + Color bg = make_color(24, 24, 24, 255) + Color orange = make_color(255, 102, 0, 255) + Color white = make_color(238, 238, 238, 255) + + while not WindowShouldClose(): + float t = GetTime() + int32 cx = 400 + lrint_f(sin_f(t * 2.0) * 150.0) + int32 cy = 225 + lrint_f(cos_f(t * 3.0) * 100.0) + + BeginDrawing() + ClearBackground(bg) + DrawCircle(cx, cy, 40.0, orange) + DrawText("Hello from Lavina!", 260, 20, 30, white) + EndDrawing() + + CloseWindow() diff --git a/examples/complex/webserver/lavina.pkg b/examples/complex/webserver/lavina.pkg index e060b16..816ecce 100644 --- a/examples/complex/webserver/lavina.pkg +++ b/examples/complex/webserver/lavina.pkg @@ -1,4 +1 @@ -name = "webserver" -deps = [ - "https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h" -] +dep httplib https://github.com/yhirose/cpp-httplib v0.18.3 httplib.h \ No newline at end of file diff --git a/lvpkg/commands.lv b/lvpkg/commands.lv index 03119f7..7505510 100644 --- a/lvpkg/commands.lv +++ b/lvpkg/commands.lv @@ -3,6 +3,7 @@ string DEPS_DIR = "deps" string DEPS_SRC = "deps/src" string DEPS_INCLUDE = "deps/include" +string DEPS_LIB = "deps/lib" // Copy relevant files from a cloned dep into deps/include/ void fn link_dep(ref Dep dep): @@ -26,6 +27,34 @@ void fn link_dep(ref Dep dep): copy_dir_recursive(src_dir, dst) info("${dep.name}: copied repo → include/${dep.name}/") +// Copy library files and headers from a cloned lib dep +void fn link_lib_dep(ref Dep dep): + string src_dir = "${DEPS_SRC}/${dep.name}" + + // Copy library file to deps/lib/ + string lib_src = "${src_dir}/${dep.lib_path}" + if ends_with(dep.lib_path, "/"): + // directory of libs — copy all into deps/lib/ + copy_dir_recursive(lib_src, DEPS_LIB) + info("${dep.name}: copied ${dep.lib_path} → lib/") + else: + // single lib file + string lib_filename = basename(dep.lib_path) + copy_file(lib_src, "${DEPS_LIB}/${lib_filename}") + info("${dep.name}: copied ${lib_filename} → lib/") + + // Copy headers if specified + if dep.path != "": + string hdr_src = "${src_dir}/${dep.path}" + if ends_with(dep.path, "/"): + string dst = "${DEPS_INCLUDE}/${dep.name}" + copy_dir_recursive(hdr_src, dst) + info("${dep.name}: copied ${dep.path} → include/${dep.name}/") + else: + string hdr_filename = basename(dep.path) + copy_file(hdr_src, "${DEPS_INCLUDE}/${hdr_filename}") + info("${dep.name}: copied ${hdr_filename} → include/") + // Install all dependencies: clone if missing, then link void fn cmd_install(): auto deps = parse_pkg_file() @@ -40,16 +69,26 @@ void fn cmd_install(): ensure_dir(DEPS_SRC) ensure_dir(DEPS_INCLUDE) + bool has_libs = false + for ref dep in deps: + if dep.dep_type == "lib": + has_libs = true + if has_libs: + ensure_dir(DEPS_LIB) + for ref dep in deps: string dest = "${DEPS_SRC}/${dep.name}" if fs_is_dir(dest): info("${dep.name}: already cloned, skipping (use 'update' to refresh)") else: git_clone(dep.url, dest, dep.version) - link_dep(dep) + if dep.dep_type == "lib": + link_lib_dep(dep) + else: + link_dep(dep) print("") - print("${C_GREEN}${C_BOLD}Done!${C_RESET} Use ${C_CYAN}-I deps/include${C_RESET} when compiling.") + print("${C_GREEN}${C_BOLD}Done!${C_RESET}") // Update all dependencies: fetch and checkout, then re-link void fn cmd_update(): @@ -65,13 +104,23 @@ void fn cmd_update(): ensure_dir(DEPS_SRC) ensure_dir(DEPS_INCLUDE) + bool has_libs = false + for ref dep in deps: + if dep.dep_type == "lib": + has_libs = true + if has_libs: + ensure_dir(DEPS_LIB) + for ref dep in deps: string dest = "${DEPS_SRC}/${dep.name}" if fs_is_dir(dest): git_update(dest, dep.version) else: git_clone(dep.url, dest, dep.version) - link_dep(dep) + if dep.dep_type == "lib": + link_lib_dep(dep) + else: + link_dep(dep) print("") print("${C_GREEN}${C_BOLD}Done!${C_RESET}") @@ -93,6 +142,8 @@ void fn cmd_list(): string path_info = "" if dep.path != "": path_info = " ${C_DIM}→ ${dep.path}${C_RESET}" + if dep.lib_path != "": + path_info += " ${C_DIM}lib: ${dep.lib_path}${C_RESET}" print(" ${C_CYAN}${dep.name}${C_RESET} ${dep.url} @ ${C_YELLOW}${dep.version}${C_RESET}${path_info} ${status}") diff --git a/lvpkg/dep.lv b/lvpkg/dep.lv index 99e5cc2..6d1c691 100644 --- a/lvpkg/dep.lv +++ b/lvpkg/dep.lv @@ -3,13 +3,16 @@ string PKG_FILE = "lavina.pkg" struct Dep: + string dep_type string name string url string version string path + string lib_path // Parse lavina.pkg and return a list of dependencies -// Format: dep [path] +// Format: dep [header_path] +// lib [header_path] vector[Dep] fn parse_pkg_file(): vector[Dep] deps = [] if not fs_exists(PKG_FILE): @@ -35,7 +38,23 @@ vector[Dep] fn parse_pkg_file(): if parts.len() >= 5: path = parts[4] - deps.push(Dep(name, url, version, path)) + deps.push(Dep("dep", name, url, version, path, "")) + + elif starts_with(trimmed, "lib "): + auto parts = trimmed.split(" ") + if parts.len() < 5: + warn("Invalid lib line (need: lib [header_path]): ${trimmed}") + continue + + string name = parts[1] + string url = parts[2] + string version = parts[3] + string lpath = parts[4] + string hpath = "" + if parts.len() >= 6: + hpath = parts[5] + + deps.push(Dep("lib", name, url, version, hpath, lpath)) else: warn("Unknown directive: ${trimmed}") diff --git a/lvpkg/lvpkg.lv b/lvpkg/lvpkg.lv index be6751d..e3e22b9 100644 --- a/lvpkg/lvpkg.lv +++ b/lvpkg/lvpkg.lv @@ -1,7 +1,7 @@ // lvpkg — Lavina Package Manager // // A simple dependency manager for Lavina projects. -// Pulls C++ header-only libraries and Lavina modules from GitHub. +// Pulls C++ header-only libraries, native libraries, and Lavina modules from GitHub. // // Usage: // lvpkg install Install dependencies from lavina.pkg @@ -10,11 +10,13 @@ // lvpkg clean Remove deps/ directory // // Package file format (lavina.pkg): -// dep [path] +// dep [header_path] +// lib [header_path] // // Examples: // dep httplib https://github.com/yhirose/cpp-httplib v0.18.3 httplib.h // dep json https://github.com/nlohmann/json v3.11.3 single_include/nlohmann/ +// lib raylib https://github.com/raysan5/raylib 5.0 lib/libraylib.a include/raylib.h import colors import strutil @@ -38,6 +40,8 @@ void fn print_help(): print(" ${C_CYAN}dep httplib https://github.com/yhirose/cpp-httplib v0.18.3 httplib.h${C_RESET}") print(" ${C_DIM}# directory of headers${C_RESET}") print(" ${C_CYAN}dep json https://github.com/nlohmann/json v3.11.3 single_include/nlohmann/${C_RESET}") + print(" ${C_DIM}# native library with headers${C_RESET}") + print(" ${C_CYAN}dep raylib https://github.com/raysan5/raylib 5.5 src/raylib.h${C_RESET}") void fn main(): init_colors() diff --git a/src/ast.lv b/src/ast.lv index aef2cae..d3b6e6e 100644 --- a/src/ast.lv +++ b/src/ast.lv @@ -112,4 +112,4 @@ enum Stmt: Continue(Token keyword) Pass(Token keyword) CppBlock(string code) - Extern(string header, string link_lib, vector[ExternType] types, vector[ExternFn] functions) + Extern(string header, string import_path, string link_lib, vector[ExternType] types, vector[ExternFn] functions) diff --git a/src/checker.lv b/src/checker.lv index 802ec13..74cf653 100644 --- a/src/checker.lv +++ b/src/checker.lv @@ -457,7 +457,7 @@ class Checker: Namespace(name, body, visibility): for ref ns_stmt in body: this.collect_decl(ns_stmt) - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): for ref et in types: vector[Stmt] empty_body = [] this.known_classes[et.lavina_name] = empty_body @@ -563,7 +563,7 @@ class Checker: pass CppBlock(code): pass - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): pass _: pass diff --git a/src/codegen.lv b/src/codegen.lv index fec9042..27aa957 100644 --- a/src/codegen.lv +++ b/src/codegen.lv @@ -15,6 +15,7 @@ class CppCodegen: vector[string] dynamic_vars vector[string] extern_includes vector[string] extern_link_libs + vector[string] extern_import_paths hashmap[string, string] extern_fn_names hashmap[string, string] extern_type_names hashmap[string, vector[Param]] extern_fn_params @@ -35,6 +36,7 @@ class CppCodegen: this.dynamic_vars = [] this.extern_includes = [] this.extern_link_libs = [] + this.extern_import_paths = [] this.extern_fn_names = {} this.extern_type_names = {} this.var_types = {} @@ -680,7 +682,7 @@ class CppCodegen: auto trimmed = line.trim() if trimmed != "": this.output +="${this.indent()}${trimmed}\n" - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): // Collect includes and name mappings (deduplicate) string inc_line = "" bool has_dot = header.indexOf(".") >= 0 @@ -695,7 +697,19 @@ class CppCodegen: if not already: this.extern_includes.push(inc_line) if link_lib != "": - this.extern_link_libs.push(link_lib) + bool link_already = false + for ref existing_lib in this.extern_link_libs: + if existing_lib == link_lib: + link_already = true + if not link_already: + this.extern_link_libs.push(link_lib) + if import_path != "": + bool path_already = false + for ref existing_path in this.extern_import_paths: + if existing_path == import_path: + path_already = true + if not path_already: + this.extern_import_paths.push(import_path) for ref et in types: if et.lavina_name != et.cpp_name: this.extern_type_names[et.lavina_name] = et.cpp_name @@ -1224,7 +1238,7 @@ class CppCodegen: pass else: this.emit_stmt(stmt, false) - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): pass _: this.emit_stmt(stmt, false) @@ -1251,14 +1265,14 @@ class CppCodegen: for mi in 0..this.module_stmts.len(): for ref stmt in this.module_stmts[mi]: match stmt: - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): this.emit_stmt(stmt, false) this.output = "" _: pass for ref stmt in stmts: match stmt: - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): this.emit_stmt(stmt, false) this.output = "" _: @@ -1291,7 +1305,7 @@ class CppCodegen: this.emit_stmt(stmt, false) this.declarations += this.output this.output = "" - Extern(header, link_lib, types, functions): + Extern(header, import_path, link_lib, types, functions): pass _: this.emit_stmt(stmt, false) diff --git a/src/main.lv b/src/main.lv index 28ff080..923dc23 100644 --- a/src/main.lv +++ b/src/main.lv @@ -156,6 +156,8 @@ int fn main(): vector[Stmt] stmts = [] string cpp = "" bool has_main = false + vector[string] link_libs = [] + vector[string] import_paths = [] try: auto parser = Parser(scanner.tokens) stmts = parser.parse_program() @@ -182,6 +184,8 @@ int fn main(): codegen.lambda_blocks = all_lambda_blocks cpp = codegen.generate(stmts) has_main = codegen.has_main + link_libs = codegen.extern_link_libs + import_paths = codegen.extern_import_paths catch err: print("Error: ${err.what()}") return 1 @@ -223,6 +227,22 @@ int fn main(): print("Warning: could not find runtime/lavina.h") string compile_cmd = "g++ -std=c++23 -o ${bin_path} ${cpp_path}" + for ref ip in import_paths: + compile_cmd += " -I${ip}" + if fs_exists("deps/include"): + bool has_deps = false + for ref ip in import_paths: + if ip == "deps/include": + has_deps = true + if not has_deps: + compile_cmd += " -Ideps/include" + if fs_exists("deps/lib"): + compile_cmd += " -Ldeps/lib" + for ref ll in link_libs: + if ll.indexOf("/") >= 0: + compile_cmd += " ${ll}" + else: + compile_cmd += " -l${ll}" int compile_result = os_exec(compile_cmd) if compile_result != 0: print("Compilation failed") diff --git a/src/parser.lv b/src/parser.lv index b9a19f3..d4e16ff 100644 --- a/src/parser.lv +++ b/src/parser.lv @@ -989,9 +989,16 @@ class Parser: Stmt fn extern_declaration(): auto header = this.consume(TK_STRING, "Expect header string after 'extern'.").lexeme + string import_path = "" string link_lib = "" - if this.match_any([TK_LINK]): - link_lib = this.consume(TK_STRING, "Expect library string after 'link'.").lexeme + bool parsing_options = true + while parsing_options: + if this.match_any([TK_IMPORT]): + import_path = this.consume(TK_STRING, "Expect path string after 'import'.").lexeme + elif this.match_any([TK_LINK]): + link_lib = this.consume(TK_STRING, "Expect library string after 'link'.").lexeme + else: + parsing_options = false this.consume(TK_COLON, "Expect ':' after extern header.") this.match_any([TK_NEWLINE]) this.consume(TK_INDENT, "Expect indentation to start extern body.") @@ -1024,7 +1031,7 @@ class Parser: this.match_any([TK_NEWLINE]) this.consume(TK_DEDENT, "Expect dedent to end extern body.") - return Stmt::Extern(header, link_lib, types, functions) + return Stmt::Extern(header, import_path, link_lib, types, functions) // ── Main entry point ──────────────────────────────────── diff --git a/stages/stage-latest.cpp b/stages/stage-latest.cpp index f500656..63d87d8 100644 --- a/stages/stage-latest.cpp +++ b/stages/stage-latest.cpp @@ -1169,7 +1169,7 @@ struct Stmt { struct Continue { Token keyword; }; struct Pass { Token keyword; }; struct CppBlock { std::string code; }; - struct Extern { std::string header; std::string link_lib; std::vector types; std::vector functions; }; + struct Extern { std::string header; std::string import_path; std::string link_lib; std::vector types; std::vector functions; }; std::string _tag; std::variant _data; @@ -1195,7 +1195,7 @@ struct Stmt { static Stmt make_Continue(Token keyword) { return {"Continue", Continue{keyword}}; } static Stmt make_Pass(Token keyword) { return {"Pass", Pass{keyword}}; } static Stmt make_CppBlock(std::string code) { return {"CppBlock", CppBlock{code}}; } - static Stmt make_Extern(std::string header, std::string link_lib, std::vector types, std::vector functions) { return {"Extern", Extern{header, link_lib, types, functions}}; } + static Stmt make_Extern(std::string header, std::string import_path, std::string link_lib, std::vector types, std::vector functions) { return {"Extern", Extern{header, import_path, link_lib, types, functions}}; } std::string operator[](const std::string& key) const { if (key == "_tag") return _tag; @@ -1217,6 +1217,7 @@ struct CppCodegen { std::vector dynamic_vars; std::vector extern_includes; std::vector extern_link_libs; + std::vector extern_import_paths; std::unordered_map extern_fn_names; std::unordered_map extern_type_names; std::unordered_map> extern_fn_params; @@ -1237,6 +1238,7 @@ struct CppCodegen { this->dynamic_vars = {}; this->extern_includes = {}; this->extern_link_libs = {}; + this->extern_import_paths = {}; this->extern_fn_names = {{}}; this->extern_type_names = {{}}; this->var_types = {{}}; @@ -2513,6 +2515,7 @@ struct CppCodegen { else if (std::holds_alternative::Extern>(_match_23._data)) { auto& _v = std::get::Extern>(_match_23._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -2534,7 +2537,26 @@ struct CppCodegen { this->extern_includes.push_back(inc_line); } if ((link_lib != std::string(""))) { - this->extern_link_libs.push_back(link_lib); + bool link_already = false; + for (const auto& existing_lib : this->extern_link_libs) { + if ((existing_lib == link_lib)) { + link_already = true; + } + } + if ((!link_already)) { + this->extern_link_libs.push_back(link_lib); + } + } + if ((import_path != std::string(""))) { + bool path_already = false; + for (const auto& existing_path : this->extern_import_paths) { + if ((existing_path == import_path)) { + path_already = true; + } + } + if ((!path_already)) { + this->extern_import_paths.push_back(import_path); + } } for (const auto& et : types) { if ((et.lavina_name != et.cpp_name)) { @@ -3456,6 +3478,7 @@ struct CppCodegen { else if (std::holds_alternative::Extern>(_match_51._data)) { auto& _v = std::get::Extern>(_match_51._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -3490,6 +3513,7 @@ struct CppCodegen { if (std::holds_alternative::Extern>(_match_52._data)) { auto& _v = std::get::Extern>(_match_52._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -3508,6 +3532,7 @@ struct CppCodegen { if (std::holds_alternative::Extern>(_match_53._data)) { auto& _v = std::get::Extern>(_match_53._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -3563,6 +3588,7 @@ struct CppCodegen { else if (std::holds_alternative::Extern>(_match_54._data)) { auto& _v = std::get::Extern>(_match_54._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -4339,6 +4365,7 @@ struct Checker { else if (std::holds_alternative::Extern>(_match_82._data)) { auto& _v = std::get::Extern>(_match_82._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -4581,6 +4608,7 @@ struct Checker { else if (std::holds_alternative::Extern>(_match_83._data)) { auto& _v = std::get::Extern>(_match_83._data); auto& header = _v.header; + auto& import_path = _v.import_path; auto& link_lib = _v.link_lib; auto& types = _v.types; auto& functions = _v.functions; @@ -6211,9 +6239,21 @@ struct Parser { Stmt extern_declaration() { auto header = (*this).consume(TK_STRING, std::string("Expect header string after 'extern'.")).lexeme; + std::string import_path = std::string(""); std::string link_lib = std::string(""); - if ((*this).match_any(std::vector{TK_LINK})) { - link_lib = (*this).consume(TK_STRING, std::string("Expect library string after 'link'.")).lexeme; + bool parsing_options = true; + while (parsing_options) { + if ((*this).match_any(std::vector{TK_IMPORT})) { + import_path = (*this).consume(TK_STRING, std::string("Expect path string after 'import'.")).lexeme; + } + else { + if ((*this).match_any(std::vector{TK_LINK})) { + link_lib = (*this).consume(TK_STRING, std::string("Expect library string after 'link'.")).lexeme; + } + else { + parsing_options = false; + } + } } (*this).consume(TK_COLON, std::string("Expect ':' after extern header.")); (*this).match_any(std::vector{TK_NEWLINE}); @@ -6252,7 +6292,7 @@ struct Parser { } } (*this).consume(TK_DEDENT, std::string("Expect dedent to end extern body.")); - return Stmt::make_Extern(header, link_lib, types, functions); + return Stmt::make_Extern(header, import_path, link_lib, types, functions); } std::vector parse_program() { @@ -6489,6 +6529,8 @@ int main(int argc, char* argv[]) { std::vector stmts = {}; std::string cpp = std::string(""); bool has_main = false; + std::vector link_libs = {}; + std::vector import_paths = {}; try { auto parser = Parser(scanner.tokens); stmts = parser.parse_program(); @@ -6519,6 +6561,8 @@ int main(int argc, char* argv[]) { codegen.lambda_blocks = all_lambda_blocks; cpp = codegen.generate(stmts); has_main = codegen.has_main; + link_libs = codegen.extern_link_libs; + import_paths = codegen.extern_import_paths; } catch (const std::exception& err) { print(((std::string("Error: ") + (err.what())) + std::string(""))); @@ -6563,6 +6607,31 @@ int main(int argc, char* argv[]) { } } std::string compile_cmd = ((((std::string("g++ -std=c++23 -o ") + (bin_path)) + std::string(" ")) + (cpp_path)) + std::string("")); + for (const auto& ip : import_paths) { + compile_cmd = (compile_cmd + ((std::string(" -I") + (ip)) + std::string(""))); + } + if (fs_exists(std::string("deps/include"))) { + bool has_deps = false; + for (const auto& ip : import_paths) { + if ((ip == std::string("deps/include"))) { + has_deps = true; + } + } + if ((!has_deps)) { + compile_cmd = (compile_cmd + std::string(" -Ideps/include")); + } + } + if (fs_exists(std::string("deps/lib"))) { + compile_cmd = (compile_cmd + std::string(" -Ldeps/lib")); + } + for (const auto& ll : link_libs) { + if ((lv_index_of(ll, std::string("/")) >= INT64_C(0))) { + compile_cmd = (compile_cmd + ((std::string(" ") + (ll)) + std::string(""))); + } + else { + compile_cmd = (compile_cmd + ((std::string(" -l") + (ll)) + std::string(""))); + } + } int64_t compile_result = os_exec(compile_cmd); if ((compile_result != INT64_C(0))) { print(std::string("Compilation failed")); diff --git a/tests/ext/test_ext_lib.h b/tests/ext/test_ext_lib.h new file mode 100644 index 0000000..2ae745f --- /dev/null +++ b/tests/ext/test_ext_lib.h @@ -0,0 +1,9 @@ +#pragma once + +inline int ext_add(int a, int b) { + return a + b; +} + +inline int ext_mul(int a, int b) { + return a * b; +} diff --git a/tests/test_extern.lv b/tests/test_extern.lv index c229c44..fba63dc 100644 --- a/tests/test_extern.lv +++ b/tests/test_extern.lv @@ -1,12 +1,35 @@ // Test: extern blocks for FFI + +// 1. Basic system header (no import, no link) extern "cmath": float fn sqrt(float x) = "std::sqrt" float fn fabs(float x) = "std::fabs" +// 2. Duplicate extern with same header (tests include deduplication) +extern "cmath": + float fn ceil_fn(float x) = "std::ceil" + +// 3. Extern with import only (header-only lib from a path) +// Note: import path not exercised via make test (g++ -I/tmp), but parsed correctly +extern "cmath" import "/usr/include": + float fn floor_fn(float x) = "std::floor" + +// 4. Extern with link only +extern "cmath" link "m": + float fn log_fn(float x) = "std::log" + +// 5. Extern with both import and link (import first) +extern "cmath" import "/usr/include" link "m": + float fn sin_fn(float x) = "std::sin" + +// 6. Extern with both import and link (link first — reversed order) +extern "cmath" link "m" import "/usr/include": + float fn cos_fn(float x) = "std::cos" + void fn main(): + // Test 1: basic extern float val = 16.0 float result = sqrt(val) - // sqrt(16) should be 4.0 if result > 3.99 and result < 4.01: print("PASS: sqrt(16) = 4.0") else: @@ -19,3 +42,43 @@ void fn main(): else: print("FAIL: fabs(-42.5) = " + to_string(neg)) exit(1) + + // Test 2: duplicate extern block + float c = ceil_fn(2.3) + if c > 2.99 and c < 3.01: + print("PASS: ceil(2.3) = 3.0") + else: + print("FAIL: ceil(2.3) = " + to_string(c)) + exit(1) + + // Test 3: extern with import + float f = floor_fn(2.7) + if f > 1.99 and f < 2.01: + print("PASS: floor(2.7) = 2.0") + else: + print("FAIL: floor(2.7) = " + to_string(f)) + exit(1) + + // Test 4: extern with link + float l = log_fn(1.0) + if l > -0.01 and l < 0.01: + print("PASS: log(1.0) = 0.0") + else: + print("FAIL: log(1.0) = " + to_string(l)) + exit(1) + + // Test 5: extern with import + link + float s = sin_fn(0.0) + if s > -0.01 and s < 0.01: + print("PASS: sin(0.0) = 0.0") + else: + print("FAIL: sin(0.0) = " + to_string(s)) + exit(1) + + // Test 6: extern with link + import (reversed order) + float co = cos_fn(0.0) + if co > 0.99 and co < 1.01: + print("PASS: cos(0.0) = 1.0") + else: + print("FAIL: cos(0.0) = " + to_string(co)) + exit(1) diff --git a/tests/test_extern_import.lv b/tests/test_extern_import.lv new file mode 100644 index 0000000..6c718a2 --- /dev/null +++ b/tests/test_extern_import.lv @@ -0,0 +1,22 @@ +// Test: extern import path — verifies -I flag is passed to g++ +extern "test_ext_lib.h" import "tests/ext": + int32 fn ext_add(int32 a, int32 b) + int32 fn ext_mul(int32 a, int32 b) + +void fn main(): + int32 a = 3 + int32 b = 4 + + int32 sum = ext_add(a, b) + if sum == 7: + print("PASS: ext_add(3, 4) = 7") + else: + print("FAIL: ext_add(3, 4) = " + to_string(sum)) + exit(1) + + int32 product = ext_mul(a, b) + if product == 12: + print("PASS: ext_mul(3, 4) = 12") + else: + print("FAIL: ext_mul(3, 4) = " + to_string(product)) + exit(1)