diff --git a/CMakeLists.txt b/CMakeLists.txt index 1dec3e0..64e060a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.13.2 LANGUAGES C) +project(fun VERSION 0.14.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/demo.fun b/demo.fun old mode 100644 new mode 100755 index c3d340a..1dc4e60 --- a/demo.fun +++ b/demo.fun @@ -1,16 +1,19 @@ #!/usr/bin/env fun /* - * This file is part of the Fun programming language. +* 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 + * + * Added: 2025-09-30 */ // Fun Interactive Demo // Run: FUN_LIB_DIR="$(pwd)/lib" ./build/fun demo.fun (Linux/macOS/FreeBSD) +// set FUN_LIB_DIR=%CD%\lib && build-debug\fun.exe demo.fun (Windows CMD) // $env:FUN_LIB_DIR="$PWD\lib"; .\build\fun.exe demo.fun (Windows PowerShell) // Use a stdlib helper from the repository (fallback to ./lib via preprocessor) diff --git a/examples/include_namespace.fun b/examples/include_namespace.fun new file mode 100755 index 0000000..fac81ba --- /dev/null +++ b/examples/include_namespace.fun @@ -0,0 +1,57 @@ +#!/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 + * + * Added: 2025-09-30 + */ + +// Demonstration of the include-as namespace feature. +// +// Run examples (without installing) by pointing FUN_LIB_DIR to the repo lib: +// Linux/macOS/FreeBSD: +// FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/include_namespace.fun +// Windows (CMD): +// set FUN_LIB_DIR=%CD%\lib && build-debug\fun.exe examples\include_namespace.fun +// Windows (PowerShell): +// $env:FUN_LIB_DIR="$PWD\lib"; .\build\fun.exe examples\include_namespace.fun + +// Import stdlib math helpers into alias 'm' +#include as m + +// Import a local module (this repository file) into alias 'mod' +#include "examples/namespaced_mod.fun" as mod + +print("=== include-as namespace demo ===") + +print("Using m.add and m.times from :") +print("m.add(2, 3) = " + to_string(m.add(2, 3))) +print("m.times(4, 5) = " + to_string(m.times(4, 5))) + +print("") +print("Using mod.hello and mod.Greeter from namespaced_mod.fun:") +print(mod.hello("Fun")) + +g = mod.Greeter("Hi") +g.say("World") + +print("") +print("=== done ===") + +/* Expected output: +=== include-as namespace demo === +Using m.add and m.times from : +m.add(2, 3) = 5 +m.times(4, 5) = 20 + +Using mod.hello and mod.Greeter from namespaced_mod.fun: +Hello, Fun! +Hi World + +=== done === +*/ diff --git a/examples/namespaced_mod.fun b/examples/namespaced_mod.fun new file mode 100644 index 0000000..d77f831 --- /dev/null +++ b/examples/namespaced_mod.fun @@ -0,0 +1,25 @@ +#!/usr/bin/env fun +// The shebang line makes no sense here because this is a library which will +// never be executed, but it shows that it is not wrong. + +/* +* 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 + * + * Added: 2025-09-30 + */ + +// namespaced_mod.fun +// Simple module to demonstrate include-as namespaces with functions and classes. + +fun hello(name) + return "Hello, " + to_string(name) + "!" + +class Greeter(string prefix) + // Methods must declare 'this' as the first parameter + fun say(this, name) + print(this.prefix + " " + to_string(name)) diff --git a/examples/sha256_str_demo.fun b/examples/sha256_str_demo.fun old mode 100644 new mode 100755 diff --git a/src/parser.c b/src/parser.c index eb1fb4e..bfc1bbb 100644 --- a/src/parser.c +++ b/src/parser.c @@ -94,6 +94,63 @@ static void calc_line_col(const char *src, size_t len, size_t pos, int *out_line /* --------------------------- */ +/* Namespace alias tracking (for include-as): + We scan preprocessed source for lines starting with "// __ns_alias__: " + and treat dot-calls on those identifiers as plain function calls (no implicit 'this'). */ +static char *g_ns_aliases[64]; +static int g_ns_alias_count = 0; + +static void ns_aliases_reset(void) { + for (int i = 0; i < g_ns_alias_count; ++i) { + free(g_ns_aliases[i]); + g_ns_aliases[i] = NULL; + } + g_ns_alias_count = 0; +} + +static void ns_aliases_scan(const char *src, size_t len) { + const char *marker = "// __ns_alias__: "; + size_t mlen = strlen(marker); + size_t i = 0; + while (i < len) { + /* find start of line */ + size_t ls = i; + /* move to end of line first */ + while (i < len && src[i] != '\n') i++; + size_t le = i; + /* include trailing '\n' in next iteration */ + if (i < len && src[i] == '\n') i++; + + if (le - ls >= mlen && strncmp(src + ls, marker, mlen) == 0) { + size_t p = ls + mlen; + /* read identifier */ + size_t start = p; + while (p < le && (src[p] == ' ' || src[p] == '\t')) p++; + if (p < le && (isalpha((unsigned char)src[p]) || src[p] == '_')) { + size_t q = p + 1; + while (q < le && (isalnum((unsigned char)src[q]) || src[q] == '_')) q++; + size_t n = q - p; + if (n > 0 && g_ns_alias_count < (int)(sizeof(g_ns_aliases)/sizeof(g_ns_aliases[0]))) { + char *name = (char*)malloc(n + 1); + if (name) { + memcpy(name, src + p, n); + name[n] = '\0'; + g_ns_aliases[g_ns_alias_count++] = name; + } + } + } + } + } +} + +static int is_ns_alias(const char *name) { + if (!name) return 0; + for (int i = 0; i < g_ns_alias_count; ++i) { + if (strcmp(g_ns_aliases[i], name) == 0) return 1; + } + return 0; +} + #include "parser_utils.c" /* very small global symbol table for LOAD_GLOBAL/STORE_GLOBAL */ @@ -1122,11 +1179,21 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(mname); continue; } - /* Prepare: stack has . Duplicate it to preserve 'this' across INDEX_GET */ - bytecode_add_instruction(bc, OP_DUP, 0); - bytecode_add_instruction(bc, OP_LOAD_CONST, kci); - bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: obj, func */ - bytecode_add_instruction(bc, OP_SWAP, 0); /* -> stack: func, obj (this) */ + + /* If base identifier is a namespace alias -> treat as plain function call: no implicit 'this' */ + int is_ns = is_ns_alias(name); + + if (!is_ns) { + /* Method sugar with implicit 'this' */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, kci); + bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: obj, func */ + bytecode_add_instruction(bc, OP_SWAP, 0); /* -> stack: func, obj (this) */ + } else { + /* Plain property function call: obj["mname"] -> func */ + bytecode_add_instruction(bc, OP_LOAD_CONST, kci); + bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> stack: func */ + } /* Consume '(' and parse args */ *pos = callp + 1; @@ -1141,8 +1208,8 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) } if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after arguments"); free(mname); free(name); return 0; } - /* Call with implicit 'this' (+1 arg) */ - bytecode_add_instruction(bc, OP_CALL, argc + 1); + /* Call */ + bytecode_add_instruction(bc, OP_CALL, is_ns ? argc : (argc + 1)); free(mname); continue; } else { @@ -1218,11 +1285,19 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) continue; } - /* Stack has obj: duplicate to preserve 'this' */ - bytecode_add_instruction(bc, OP_DUP, 0); - bytecode_add_instruction(bc, OP_LOAD_CONST, kci); - bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> obj, func */ - bytecode_add_instruction(bc, OP_SWAP, 0); /* -> func, obj */ + int is_ns = is_ns_alias(name); + + if (!is_ns) { + /* Method sugar with implicit 'this' */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_LOAD_CONST, kci); + bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> obj, func */ + bytecode_add_instruction(bc, OP_SWAP, 0); /* -> func, obj */ + } else { + /* Plain property function call */ + bytecode_add_instruction(bc, OP_LOAD_CONST, kci); + bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* -> func */ + } *pos = callp + 1; int argc = 0; @@ -1236,7 +1311,7 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) } if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after arguments"); free(mname); free(name); return 0; } - bytecode_add_instruction(bc, OP_CALL, argc + 1); + bytecode_add_instruction(bc, OP_CALL, is_ns ? argc : (argc + 1)); free(mname); continue; } else { @@ -3233,6 +3308,10 @@ static Bytecode *compile_minimal(const char *src, size_t len) { Bytecode *bc = bytecode_new(); size_t pos = 0; + /* Refresh namespace alias table for this compilation unit */ + ns_aliases_reset(); + ns_aliases_scan(src, len); + skip_shebang_if_present(src, len, &pos); /* Allow top-of-file function definitions; do not skip any leading 'fun' line */ diff --git a/src/parser_utils.c b/src/parser_utils.c index 1c82e21..13c0ab0 100644 --- a/src/parser_utils.c +++ b/src/parser_utils.c @@ -256,6 +256,158 @@ static void sb_append_ch(StrBuf *sb, char c) { sb->buf[sb->len] = '\0'; } +/* ---- Export collection for include-as namespaces ---- */ +typedef struct { + char **names; + int count; + int cap; +} NameList; + +static void nl_init(NameList *nl) { + nl->names = NULL; + nl->count = 0; + nl->cap = 0; +} + +static void nl_add(NameList *nl, const char *name) { + if (!name || !name[0]) return; + if (nl->count >= nl->cap) { + int ncap = nl->cap ? nl->cap * 2 : 8; + char **nn = (char**)realloc(nl->names, (size_t)ncap * sizeof(char*)); + if (!nn) return; + nl->names = nn; + nl->cap = ncap; + } + nl->names[nl->count++] = strdup(name); +} + +static void nl_free(NameList *nl) { + if (!nl) return; + for (int i = 0; i < nl->count; ++i) free(nl->names[i]); + free(nl->names); + nl->names = NULL; + nl->count = nl->cap = 0; +} + +/* Collect top-level (indent=0) exported symbols: function and class names. + Ignores lines inside comments/strings and ignores nested indent. */ +static void collect_exports_top_level(const char *text, NameList *out) { + if (!text || !out) return; + size_t len = strlen(text); + int in_line = 0, in_block = 0, in_sq = 0, in_dq = 0, esc = 0; + int bol = 1; + for (size_t i = 0; i < len; ) { + char c = text[i]; + + if (in_line) { + if (c == '\n') { in_line = 0; bol = 1; } + else { bol = 0; } + i++; + continue; + } + if (in_block) { + if (c == '*' && (i + 1) < len && text[i + 1] == '/') { + i += 2; + bol = 0; + in_block = 0; + continue; + } + bol = (c == '\n'); + i++; + continue; + } + if (in_sq) { + if (!esc && c == '\\') { esc = 1; i++; bol = 0; continue; } + if (!esc && c == '\'') { in_sq = 0; } + esc = 0; + bol = (c == '\n'); + i++; + continue; + } + if (in_dq) { + if (!esc && c == '\\') { esc = 1; i++; bol = 0; continue; } + if (!esc && c == '"') { in_dq = 0; } + esc = 0; + bol = (c == '\n'); + i++; + continue; + } + + if (c == '/' && (i + 1) < len && text[i + 1] == '/') { + in_line = 1; + bol = 0; + i += 2; + continue; + } + if (c == '/' && (i + 1) < len && text[i + 1] == '*') { + in_block = 1; + bol = 0; + i += 2; + continue; + } + if (c == '\'') { in_sq = 1; bol = 0; i++; continue; } + if (c == '"') { in_dq = 1; bol = 0; i++; continue; } + + if (bol) { + /* Compute leading spaces to filter out indented constructs */ + size_t j = i; + int spaces = 0; + while (j < len && text[j] == ' ') { spaces++; j++; } + if (j < len && text[j] == '\t') { + /* tabs not allowed for indentation; treat as not top-level */ + bol = 0; + i = j + 1; + continue; + } + /* Only consider top-level (indent == 0) */ + if (spaces == 0) { + /* Check for 'fun ' or 'class ' */ + const char *kw1 = "fun"; + const char *kw2 = "class"; + if (j + 3 <= len && strncmp(text + j, kw1, 3) == 0 && (j + 3 == len || isspace((unsigned char)text[j + 3]))) { + size_t p = j + 3; + while (p < len && (text[p] == ' ' || text[p] == '\t')) p++; + /* read identifier */ + size_t start = p; + if (p < len && (isalpha((unsigned char)text[p]) || text[p] == '_')) { + p++; + while (p < len && (isalnum((unsigned char)text[p]) || text[p] == '_')) p++; + size_t n = p - start; + if (n > 0) { + char tmp[256]; + size_t copy = (n < sizeof(tmp) - 1) ? n : (sizeof(tmp) - 1); + memcpy(tmp, text + start, copy); + tmp[copy] = '\0'; + nl_add(out, tmp); + } + } + } else if (j + 5 <= len && strncmp(text + j, kw2, 5) == 0 && (j + 5 == len || isspace((unsigned char)text[j + 5]))) { + size_t p = j + 5; + while (p < len && (text[p] == ' ' || text[p] == '\t')) p++; + /* read identifier */ + size_t start = p; + if (p < len && (isalpha((unsigned char)text[p]) || text[p] == '_')) { + p++; + while (p < len && (isalnum((unsigned char)text[p]) || text[p] == '_')) p++; + size_t n = p - start; + if (n > 0) { + char tmp[256]; + size_t copy = (n < sizeof(tmp) - 1) ? n : (sizeof(tmp) - 1); + memcpy(tmp, text + start, copy); + tmp[copy] = '\0'; + nl_add(out, tmp); + } + } + } + } + } + + /* move forward one char */ + bol = (c == '\n'); + i++; + } +} + static char *preprocess_includes_internal(const char *src, int depth) { if (!src) return NULL; if (depth > 64) { @@ -304,8 +456,31 @@ static char *preprocess_includes_internal(const char *src, int depth) { memcpy(path, src + path_start, path_len); path[path_len] = '\0'; - /* advance to end of line */ + /* parse optional 'as ' then advance to end of line */ k++; + char ns[64]; ns[0] = '\0'; + /* skip spaces/tabs */ + size_t ap = k; + while (ap < len && (src[ap] == ' ' || src[ap] == '\t')) ap++; + /* optional 'as' */ + const char *askw = "as"; + if (ap + 2 <= len && strncmp(src + ap, askw, 2) == 0 && (ap + 2 == len || isspace((unsigned char)src[ap + 2]))) { + ap += 2; + while (ap < len && (src[ap] == ' ' || src[ap] == '\t')) ap++; + /* read identifier [A-Za-z_][A-Za-z0-9_]* */ + size_t start = ap; + if (ap < len && (isalpha((unsigned char)src[ap]) || src[ap] == '_')) { + ap++; + while (ap < len && (isalnum((unsigned char)src[ap]) || src[ap] == '_')) ap++; + size_t n = ap - start; + size_t copy = (n < sizeof(ns) - 1) ? n : (sizeof(ns) - 1); + memcpy(ns, src + start, copy); + ns[copy] = '\0'; + } + /* ignore anything else on line */ + } + /* advance to end of line */ + k = ap; while (k < len && src[k] != '\n') k++; if (k < len && src[k] == '\n') k++; @@ -362,16 +537,66 @@ static char *preprocess_includes_internal(const char *src, int depth) { sb_append(&out, resolved[0] ? resolved : "(unresolved)"); sb_append(&out, "\n"); } else { - char *exp = preprocess_includes_internal(inc, depth + 1); + /* Strip optional UTF-8 BOM and top-of-file shebang from included text before preprocessing */ + const char *startp = inc; + size_t off = 0; + if ((unsigned char)inc[0] == 0xEF && (unsigned char)inc[1] == 0xBB && (unsigned char)inc[2] == 0xBF) { + off = 3; + } + startp = inc + off; + if (startp[0] == '#' && startp[1] == '!') { + /* skip until end of line, handling CR, LF, CRLF */ + const char *q = startp; + while (*q && *q != '\n' && *q != '\r') q++; + if (*q == '\r') { q++; if (*q == '\n') q++; } + else if (*q == '\n') { q++; } + startp = q; + } + char *inc_clean = strdup(startp); + char *exp = preprocess_includes_internal(inc_clean, depth + 1); free(inc); + free(inc_clean); if (exp) { + /* If alias requested, initialize namespace map before including content */ + if (ns[0] != '\0') { + /* Announce alias so the parser can treat dot-call without implicit 'this' */ + sb_append(&out, "// __ns_alias__: "); + sb_append(&out, ns); + sb_append(&out, "\n"); + + sb_append(&out, ns); + sb_append(&out, " = {}\n"); + } + /* mark file origin for better error messages */ sb_append(&out, "// __include_begin__: "); sb_append(&out, resolved); + if (ns[0] != '\0') { + sb_append(&out, " as "); + sb_append(&out, ns); + } sb_append(&out, "\n"); + + /* append expanded included content */ 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'); + + /* If alias is present, export top-level fun/class into alias map */ + if (ns[0] != '\0') { + NameList nl; nl_init(&nl); + collect_exports_top_level(exp, &nl); + for (int ei = 0; ei < nl.count; ++ei) { + sb_append(&out, ns); + sb_append(&out, "."); + sb_append(&out, nl.names[ei]); + sb_append(&out, " = "); + sb_append(&out, nl.names[ei]); + sb_append(&out, "\n"); + } + nl_free(&nl); + } + free(exp); } }