diff --git a/CMakeLists.txt b/CMakeLists.txt index 599332c..783a962 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.5.2 LANGUAGES C) +project(fun VERSION 0.6.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -68,6 +68,10 @@ add_custom_target(distclean install(TARGETS fun RUNTIME DESTINATION /usr/bin) +install(DIRECTORY lib/ + DESTINATION /usr/lib/fun + FILES_MATCHING PATTERN "*.fun") + # - Optionally install example scripts option(FUN_INSTALL_EXAMPLES "Install example .fun scripts" ON) if(FUN_INSTALL_EXAMPLES) diff --git a/examples/include_lib.fun b/examples/include_lib.fun new file mode 100644 index 0000000..f6468b2 --- /dev/null +++ b/examples/include_lib.fun @@ -0,0 +1,24 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +// Demonstrates system library includes after installation to /usr/lib/fun + +include +include + +print("== include lib demo ==") +hello_lib() + +number x = 10 +number y = 32 + +print("add(" + to_string(x) + ", " + to_string(y) + ") = " + to_string(add(x, y))) +print("times(" + to_string(x) + ", " + to_string(y) + ") = " + to_string(times(x, y))) diff --git a/examples/include_local.fun b/examples/include_local.fun new file mode 100644 index 0000000..51df0a2 --- /dev/null +++ b/examples/include_local.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +// To test this you need to go to ./examples/ and test includes running ../build/fun include_local.fun +// Local includes are always relative from the $PWD. + +// Demonstrates local include: +// This resolves relative to the current working directory where 'fun' is executed. +#include "include_local_util.fun" + +print("== include local demo ==") +greet("Fun") + +number a = 2 +number b = 3 +print("sum(" + to_string(a) + ", " + to_string(b) + ") = " + to_string(sum(a, b))) + +// Tip: +// To include from the system library directory (/usr/lib/fun), use angle brackets: +// #include + diff --git a/examples/include_local_util.fun b/examples/include_local_util.fun new file mode 100644 index 0000000..ca4ef60 --- /dev/null +++ b/examples/include_local_util.fun @@ -0,0 +1,16 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +// Simple utility module to be included locally with: #include "include_local_util.fun" + +fun greet(name) + print("Hello, " + name) + +fun sum(a, b) + return a + b diff --git a/lib/hello.fun b/lib/hello.fun new file mode 100644 index 0000000..72f69fb --- /dev/null +++ b/lib/hello.fun @@ -0,0 +1,17 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +/* + * A tiny system library module installed to /usr/lib/fun + * Usage (after installation): + * #include +*/ + +fun hello_lib() + print("Hello from system lib!") diff --git a/lib/utils/math.fun b/lib/utils/math.fun new file mode 100644 index 0000000..9147e85 --- /dev/null +++ b/lib/utils/math.fun @@ -0,0 +1,17 @@ +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +// Basic math helpers to be included from system lib: +// #include + +fun add(a, b) + return a + b + +fun times(a, b) + return a * b diff --git a/src/parser.c b/src/parser.c index 0308c3d..f6deb36 100644 --- a/src/parser.c +++ b/src/parser.c @@ -3009,6 +3009,12 @@ Bytecode *parse_file_to_bytecode(const char *path) { fprintf(stderr, "Error: cannot read file: %s\n", path); return NULL; } + + /* Preprocess includes before compiling */ + char *prep = preprocess_includes(src); + const char *compile_src = prep ? prep : src; + size_t compile_len = strlen(compile_src); + /* reset error state */ g_has_error = 0; g_err_pos = 0; @@ -3016,19 +3022,21 @@ Bytecode *parse_file_to_bytecode(const char *path) { g_err_line = 0; g_err_col = 0; - Bytecode *bc = compile_minimal(src, len); + Bytecode *bc = compile_minimal(compile_src, compile_len); if (g_has_error) { int line = 1, col = 1; - calc_line_col(src, len, g_err_pos, &line, &col); + calc_line_col(compile_src, compile_len, g_err_pos, &line, &col); g_err_line = line; g_err_col = col; fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "", line, col, g_err_msg); if (bc) bytecode_free(bc); + if (prep) free(prep); free(src); return NULL; } + if (prep) free(prep); free(src); return bc; } @@ -3038,7 +3046,11 @@ Bytecode *parse_string_to_bytecode(const char *source) { fprintf(stderr, "Error: null source provided\n"); return NULL; } - size_t len = strlen(source); + + /* Preprocess includes before compiling */ + char *prep = preprocess_includes(source); + const char *compile_src = prep ? prep : source; + size_t len = strlen(compile_src); /* reset error state */ g_has_error = 0; @@ -3047,16 +3059,18 @@ Bytecode *parse_string_to_bytecode(const char *source) { g_err_line = 0; g_err_col = 0; - Bytecode *bc = compile_minimal(source, len); + Bytecode *bc = compile_minimal(compile_src, len); if (g_has_error) { int line = 1, col = 1; - calc_line_col(source, len, g_err_pos, &line, &col); + calc_line_col(compile_src, len, g_err_pos, &line, &col); g_err_line = line; g_err_col = col; if (bc) bytecode_free(bc); + if (prep) free(prep); return NULL; } + if (prep) free(prep); return bc; } diff --git a/src/parser_utils.c b/src/parser_utils.c index 3fd4cbf..74b722f 100644 --- a/src/parser_utils.c +++ b/src/parser_utils.c @@ -177,3 +177,232 @@ static int64_t parse_int_literal_value(const char *src, size_t len, size_t *pos, *ok = 1; return sign * val; } + +/* === Include preprocessor === + * Supports: + * - #include "path" (resolved relative to current working directory) + * - #include (resolved under /usr/lib/fun/) + * Also accepts 'include' without '#', at the start of a line (after spaces/tabs). + * Directives are recognized only when not inside strings or block comments. + */ + +static void *xrealloc(void *ptr, size_t newcap) { + void *np = realloc(ptr, newcap); + return np; +} + +typedef struct { + char *buf; + size_t len; + size_t cap; +} StrBuf; + +static void sb_init(StrBuf *sb) { + sb->buf = (char*)malloc(256); + sb->cap = sb->buf ? 256 : 0; + sb->len = 0; + if (sb->buf) sb->buf[0] = '\0'; +} + +static void sb_reserve(StrBuf *sb, size_t need) { + if (need <= sb->cap) return; + size_t nc = sb->cap ? sb->cap : 256; + while (nc < need) nc *= 2; + char *nb = (char*)xrealloc(sb->buf, nc); + if (!nb) return; + sb->buf = nb; + sb->cap = nc; +} + +static void sb_append_n(StrBuf *sb, const char *s, size_t n) { + if (n == 0) return; + sb_reserve(sb, sb->len + n + 1); + if (!sb->buf) return; + memcpy(sb->buf + sb->len, s, n); + sb->len += n; + sb->buf[sb->len] = '\0'; +} + +static void sb_append(StrBuf *sb, const char *s) { + sb_append_n(sb, s, strlen(s)); +} + +static void sb_append_ch(StrBuf *sb, char c) { + sb_reserve(sb, sb->len + 2); + if (!sb->buf) return; + sb->buf[sb->len++] = c; + sb->buf[sb->len] = '\0'; +} + +static char *preprocess_includes_internal(const char *src, int depth) { + if (!src) return NULL; + if (depth > 64) { + fprintf(stderr, "Include error: include nesting too deep\n"); + return strdup(""); + } + + const char *LIB_DIR = "/usr/lib/fun/"; + size_t len = strlen(src); + StrBuf out; + sb_init(&out); + int in_line = 0, in_block = 0, in_sq = 0, in_dq = 0, esc = 0; + int bol = 1; /* beginning of line */ + + for (size_t i = 0; i < len; ) { + char c = src[i]; + + /* Detect include directive at BOL, outside comments/strings */ + if (bol && !in_block && !in_sq && !in_dq) { + size_t j = i; + /* skip leading spaces/tabs */ + while (j < len && (src[j] == ' ' || src[j] == '\t')) j++; + size_t k = j; + if (k < len && src[k] == '#') k++; + const char *kw = "include"; + size_t kwlen = 7; + if (k + kwlen <= len && strncmp(src + k, kw, kwlen) == 0) { + k += kwlen; + /* next must be space/tab or delimiter */ + while (k < len && (src[k] == ' ' || src[k] == '\t')) k++; + if (k < len && (src[k] == '"' || src[k] == '<')) { + char opener = src[k]; + char closer = (opener == '"') ? '"' : '>'; + k++; + size_t path_start = k; + while (k < len && src[k] != closer) k++; + if (k < len && src[k] == closer) { + size_t path_len = k - path_start; + char *path = (char*)malloc(path_len + 1); + if (path) { + memcpy(path, src + path_start, path_len); + path[path_len] = '\0'; + + /* advance to end of line */ + k++; + while (k < len && src[k] != '\n') k++; + if (k < len && src[k] == '\n') k++; + + /* resolve file path */ + char resolved[1024]; + if (opener == '<') { + snprintf(resolved, sizeof(resolved), "%s%s", LIB_DIR, path); + } else { + snprintf(resolved, sizeof(resolved), "%s", path); + } + free(path); + + /* read and recursively expand */ + size_t inc_len = 0; + char *inc = read_file_all(resolved, &inc_len); + if (!inc) { + fprintf(stderr, "Include error: cannot read '%s'\n", resolved); + sb_append(&out, "// include error: cannot read "); + sb_append(&out, resolved); + sb_append(&out, "\n"); + } else { + char *exp = preprocess_includes_internal(inc, depth + 1); + free(inc); + if (exp) { + sb_append(&out, exp); + /* ensure included chunk ends with newline to preserve line structure */ + if (out.len == 0 || out.buf[out.len - 1] != '\n') sb_append_ch(&out, '\n'); + free(exp); + } + } + + /* move input pointer to start of next line */ + i = k; + bol = 1; + continue; + } + } + } + } + } + + /* normal stateful copy with comment/string tracking */ + if (in_line) { + sb_append_ch(&out, c); + if (c == '\n') { in_line = 0; bol = 1; } else { bol = 0; } + i++; + continue; + } + if (in_block) { + sb_append_ch(&out, c); + if (c == '*' && (i + 1) < len && src[i + 1] == '/') { + sb_append_ch(&out, '/'); + i += 2; + bol = 0; + in_block = 0; + continue; + } + bol = (c == '\n') ? 1 : 0; + i++; + continue; + } + if (in_sq) { + sb_append_ch(&out, c); + if (!esc && c == '\\') { esc = 1; i++; bol = 0; continue; } + if (!esc && c == '\'') { in_sq = 0; } + esc = 0; + bol = (c == '\n') ? 1 : 0; + i++; + continue; + } + if (in_dq) { + sb_append_ch(&out, c); + if (!esc && c == '\\') { esc = 1; i++; bol = 0; continue; } + if (!esc && c == '"') { in_dq = 0; } + esc = 0; + bol = (c == '\n') ? 1 : 0; + i++; + continue; + } + + /* outside any special state */ + if (c == '/' && (i + 1) < len && src[i + 1] == '/') { + sb_append_ch(&out, '/'); + sb_append_ch(&out, '/'); + i += 2; + in_line = 1; + bol = 0; + continue; + } + if (c == '/' && (i + 1) < len && src[i + 1] == '*') { + sb_append_ch(&out, '/'); + sb_append_ch(&out, '*'); + i += 2; + in_block = 1; + bol = 0; + continue; + } + if (c == '\'') { + sb_append_ch(&out, c); + in_sq = 1; + bol = 0; + i++; + continue; + } + if (c == '"') { + sb_append_ch(&out, c); + in_dq = 1; + bol = 0; + i++; + continue; + } + + sb_append_ch(&out, c); + bol = (c == '\n') ? 1 : 0; + i++; + } + + if (!out.buf) return strdup(""); + /* ensure NUL-terminated */ + if (out.cap == out.len) sb_reserve(&out, out.len + 1); + if (out.buf) out.buf[out.len] = '\0'; + return out.buf; +} + +static char *preprocess_includes(const char *src) { + return preprocess_includes_internal(src, 0); +}