From cee3acef15105ff835d24e0e9b162adb2ada655b Mon Sep 17 00:00:00 2001 From: soumalya das Date: Sat, 10 Oct 2026 20:36:13 +0530 Subject: [PATCH 1/4] Support package manifest lib entry points --- src/runtime/sys.c | 169 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 132 insertions(+), 37 deletions(-) diff --git a/src/runtime/sys.c b/src/runtime/sys.c index 9e26a09..2b95177 100644 --- a/src/runtime/sys.c +++ b/src/runtime/sys.c @@ -437,6 +437,102 @@ static char *pkg_candidate(const char *path) return NULL; } +/* + * An installed package may explicitly choose its public module with + * [package] lib = "src/api.fl". The path is relative to the package root; + * it must stay inside that root and name a .fl file. A declared but invalid + * or missing entry point does not silently fall back to a different file. + */ +static char *pkg_declared_lib(const char *package_dir, bool *declared) +{ + *declared = false; + char manifest[8192]; + if (snprintf(manifest, sizeof(manifest), "%s/flint.toml", package_dir) >= + (int)sizeof(manifest)) + return NULL; + FILE *file = fopen(manifest, "r"); + if (file == NULL) + return NULL; + + bool in_package = false; + char line[4096]; + char *entry = NULL; + while (fgets(line, sizeof(line), file) != NULL) { + char *p = line; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '\0' || *p == '\n' || *p == '#') + continue; + if (*p == '[') { + char *end = strchr(p, ']'); + if (end == NULL) + continue; + end[1] = '\0'; + in_package = strcmp(p, "[package]") == 0; + continue; + } + if (!in_package || strncmp(p, "lib", 3) != 0 || + (p[3] != ' ' && p[3] != '\t' && p[3] != '=')) + continue; + p += 3; + while (*p == ' ' || *p == '\t') + p++; + if (*p++ != '=') + continue; + while (*p == ' ' || *p == '\t') + p++; + *declared = true; + if (*p++ != '"') + break; + char *end = strchr(p, '"'); + if (end == NULL) + break; + char *tail = end + 1; + while (*tail == ' ' || *tail == '\t' || *tail == '\r' || + *tail == '\n') + tail++; + if (*tail != '\0' && *tail != '#') + break; + *end = '\0'; + size_t len = strlen(p); + if (len < 4 || strcmp(p + len - 3, ".fl") != 0 || + p[0] == '/' || strchr(p, '\\') != NULL) + break; + bool unsafe = false; + const char *part = p; + for (const char *q = p;; q++) { + if (*q == '/' || *q == '\0') { + size_t part_len = (size_t)(q - part); + if (part_len == 0 || + (part_len == 1 && part[0] == '.') || + (part_len == 2 && part[0] == '.' && + part[1] == '.')) { + unsafe = true; + break; + } + if (*q == '\0') + break; + part = q + 1; + } + } + if (unsafe) + break; + size_t dir_len = strlen(package_dir); + if (dir_len > (size_t)-1 - len - 2) + break; + entry = malloc(dir_len + len + 2); + if (entry != NULL) + snprintf(entry, dir_len + len + 2, "%s/%s", package_dir, p); + break; + } + fclose(file); + if (entry == NULL) + return NULL; + char *found = pkg_candidate(entry); + free(entry); + return found; +} + char *sys_resolve_module(const char *path) { size_t pathlen = strlen(path); @@ -471,44 +567,43 @@ char *sys_resolve_module(const char *path) pkg_join3(root, "flint_modules", path); char *found = NULL; if (dir_main != NULL) { - size_t need = strlen(dir_main) + 9; - char *main_file = malloc(need); - if (main_file != NULL) { - snprintf(main_file, - need, - "%s/main.fl", - dir_main); - found = pkg_candidate( - main_file); - free(main_file); - } - /* - * Also support a single-file package named after - * itself, e.g. levenshtein/levenshtein.fl. - */ - if (found == NULL) { - size_t need_named = strlen(dir_main) + 1 + - pathlen + 3 + 1; - char *named = malloc(need_named); - if (named != NULL) { - snprintf(named, need_named, "%s/%s.fl", - dir_main, path); - found = pkg_candidate(named); - free(named); - } - } - if (found == NULL) { - size_t need2 = - strlen(dir_main) + 4; - char *flat = malloc(need2); - if (flat != NULL) { - snprintf(flat, - need2, - "%s.fl", + bool has_declared_lib = false; + found = pkg_declared_lib( + dir_main, &has_declared_lib); + if (!has_declared_lib) { + size_t need = strlen(dir_main) + 9; + char *main_file = malloc(need); + if (main_file != NULL) { + snprintf(main_file, + need, + "%s/main.fl", dir_main); - found = pkg_candidate( - flat); - free(flat); + found = pkg_candidate(main_file); + free(main_file); + } + /* + * Also support a single-file package named after + * itself, e.g. levenshtein/levenshtein.fl. + */ + if (found == NULL) { + size_t need_named = strlen(dir_main) + 1 + + pathlen + 3 + 1; + char *named = malloc(need_named); + if (named != NULL) { + snprintf(named, need_named, "%s/%s.fl", + dir_main, path); + found = pkg_candidate(named); + free(named); + } + } + if (found == NULL) { + size_t need2 = strlen(dir_main) + 4; + char *flat = malloc(need2); + if (flat != NULL) { + snprintf(flat, need2, "%s.fl", dir_main); + found = pkg_candidate(flat); + free(flat); + } } } free(dir_main); From 9dc8cd67c01db4a7fddf20d42d1c99dfa6df2c6b Mon Sep 17 00:00:00 2001 From: soumalya das Date: Sat, 10 Oct 2026 20:36:15 +0530 Subject: [PATCH 2/4] Sync embedded package lib entry points --- rust/flintlang/c-src/src/runtime/sys.c | 169 +++++++++++++++++++------ 1 file changed, 132 insertions(+), 37 deletions(-) diff --git a/rust/flintlang/c-src/src/runtime/sys.c b/rust/flintlang/c-src/src/runtime/sys.c index 9e26a09..2b95177 100644 --- a/rust/flintlang/c-src/src/runtime/sys.c +++ b/rust/flintlang/c-src/src/runtime/sys.c @@ -437,6 +437,102 @@ static char *pkg_candidate(const char *path) return NULL; } +/* + * An installed package may explicitly choose its public module with + * [package] lib = "src/api.fl". The path is relative to the package root; + * it must stay inside that root and name a .fl file. A declared but invalid + * or missing entry point does not silently fall back to a different file. + */ +static char *pkg_declared_lib(const char *package_dir, bool *declared) +{ + *declared = false; + char manifest[8192]; + if (snprintf(manifest, sizeof(manifest), "%s/flint.toml", package_dir) >= + (int)sizeof(manifest)) + return NULL; + FILE *file = fopen(manifest, "r"); + if (file == NULL) + return NULL; + + bool in_package = false; + char line[4096]; + char *entry = NULL; + while (fgets(line, sizeof(line), file) != NULL) { + char *p = line; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '\0' || *p == '\n' || *p == '#') + continue; + if (*p == '[') { + char *end = strchr(p, ']'); + if (end == NULL) + continue; + end[1] = '\0'; + in_package = strcmp(p, "[package]") == 0; + continue; + } + if (!in_package || strncmp(p, "lib", 3) != 0 || + (p[3] != ' ' && p[3] != '\t' && p[3] != '=')) + continue; + p += 3; + while (*p == ' ' || *p == '\t') + p++; + if (*p++ != '=') + continue; + while (*p == ' ' || *p == '\t') + p++; + *declared = true; + if (*p++ != '"') + break; + char *end = strchr(p, '"'); + if (end == NULL) + break; + char *tail = end + 1; + while (*tail == ' ' || *tail == '\t' || *tail == '\r' || + *tail == '\n') + tail++; + if (*tail != '\0' && *tail != '#') + break; + *end = '\0'; + size_t len = strlen(p); + if (len < 4 || strcmp(p + len - 3, ".fl") != 0 || + p[0] == '/' || strchr(p, '\\') != NULL) + break; + bool unsafe = false; + const char *part = p; + for (const char *q = p;; q++) { + if (*q == '/' || *q == '\0') { + size_t part_len = (size_t)(q - part); + if (part_len == 0 || + (part_len == 1 && part[0] == '.') || + (part_len == 2 && part[0] == '.' && + part[1] == '.')) { + unsafe = true; + break; + } + if (*q == '\0') + break; + part = q + 1; + } + } + if (unsafe) + break; + size_t dir_len = strlen(package_dir); + if (dir_len > (size_t)-1 - len - 2) + break; + entry = malloc(dir_len + len + 2); + if (entry != NULL) + snprintf(entry, dir_len + len + 2, "%s/%s", package_dir, p); + break; + } + fclose(file); + if (entry == NULL) + return NULL; + char *found = pkg_candidate(entry); + free(entry); + return found; +} + char *sys_resolve_module(const char *path) { size_t pathlen = strlen(path); @@ -471,44 +567,43 @@ char *sys_resolve_module(const char *path) pkg_join3(root, "flint_modules", path); char *found = NULL; if (dir_main != NULL) { - size_t need = strlen(dir_main) + 9; - char *main_file = malloc(need); - if (main_file != NULL) { - snprintf(main_file, - need, - "%s/main.fl", - dir_main); - found = pkg_candidate( - main_file); - free(main_file); - } - /* - * Also support a single-file package named after - * itself, e.g. levenshtein/levenshtein.fl. - */ - if (found == NULL) { - size_t need_named = strlen(dir_main) + 1 + - pathlen + 3 + 1; - char *named = malloc(need_named); - if (named != NULL) { - snprintf(named, need_named, "%s/%s.fl", - dir_main, path); - found = pkg_candidate(named); - free(named); - } - } - if (found == NULL) { - size_t need2 = - strlen(dir_main) + 4; - char *flat = malloc(need2); - if (flat != NULL) { - snprintf(flat, - need2, - "%s.fl", + bool has_declared_lib = false; + found = pkg_declared_lib( + dir_main, &has_declared_lib); + if (!has_declared_lib) { + size_t need = strlen(dir_main) + 9; + char *main_file = malloc(need); + if (main_file != NULL) { + snprintf(main_file, + need, + "%s/main.fl", dir_main); - found = pkg_candidate( - flat); - free(flat); + found = pkg_candidate(main_file); + free(main_file); + } + /* + * Also support a single-file package named after + * itself, e.g. levenshtein/levenshtein.fl. + */ + if (found == NULL) { + size_t need_named = strlen(dir_main) + 1 + + pathlen + 3 + 1; + char *named = malloc(need_named); + if (named != NULL) { + snprintf(named, need_named, "%s/%s.fl", + dir_main, path); + found = pkg_candidate(named); + free(named); + } + } + if (found == NULL) { + size_t need2 = strlen(dir_main) + 4; + char *flat = malloc(need2); + if (flat != NULL) { + snprintf(flat, need2, "%s.fl", dir_main); + found = pkg_candidate(flat); + free(flat); + } } } free(dir_main); From 91c14cad6db70c9681738635e3ae6de09ce768a2 Mon Sep 17 00:00:00 2001 From: soumalya das Date: Sat, 10 Oct 2026 20:36:17 +0530 Subject: [PATCH 3/4] Document package lib manifest field --- docs/packages.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/packages.md b/docs/packages.md index 730dacb..98e6190 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -18,7 +18,12 @@ libbar = { path = "../libbar", version = "^1.0.0" } gitlib = { git = "https://example.com/gitlib.git", rev = "v1.0.0" } ``` -- `[package]` carries `name` and `version`. +- `[package]` carries `name` and `version`. Optional `lib = "src/api.fl"` + selects the package's public module entry point, relative to the package root. + The path must name a `.fl` file inside the package. When `lib` is set, a + missing or invalid target is an error at import time; Flint does not silently + choose `main.fl` instead. Without `lib`, the legacy `main.fl`, same-named, + and flat-file layouts continue to work. - `[dependencies]` maps a name to a path string, a `{ path, version }` table, or a `{ git, rev, version }` table. `rev` may be empty, which tracks the default branch; the lock always pins the commit installed. @@ -81,9 +86,17 @@ installs; the next install records the hashes. `pkg list` output is unchanged. Each dependency lands in `flint_modules//` with its sources and -its own `flint.toml`. A package can expose its default module as -`main.fl`, as a same-named file such as `levenshtein/levenshtein.fl`, -or as a flat `flint_modules/.fl` file. +its own `flint.toml`. A package may declare its public module explicitly: + +```toml +[package] +name = "levenshtein" +version = "1.0.0" +lib = "src/levenshtein.fl" +``` + +Otherwise, Flint keeps the compatibility layouts: `main.fl`, a same-named +file such as `levenshtein/levenshtein.fl`, or a flat `flint_modules/.fl` file. ```text flint_modules/ @@ -108,8 +121,9 @@ import "gitlib/g.fl" print(g.greet()) ``` -A bare name first checks installed packages in `flint_modules/`: -`/main.fl`, then `/.fl`, then the flat +A bare name first checks installed packages in `flint_modules/`. If the +package manifest declares `lib`, that path is the sole entry point. Otherwise, +resolution checks `/main.fl`, then `/.fl`, then the flat `.fl` layout. If no package matches, the standard library is checked (so `import math` still imports the library). A quoted path with a `/` or a `.fl` extension can address a file relative to the importing From e3934c18fc532f26aa44fdc5bb627df112277804 Mon Sep 17 00:00:00 2001 From: soumalya das Date: Sat, 10 Oct 2026 20:36:19 +0530 Subject: [PATCH 4/4] Test explicit package library entry point --- tests/pkg_test.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/pkg_test.sh b/tests/pkg_test.sh index 66b9603..9d114b9 100644 --- a/tests/pkg_test.sh +++ b/tests/pkg_test.sh @@ -85,6 +85,39 @@ out=$("$FLINT" main.fl 2>&1) expect "package import works" "hi sam HI SAM" "$out" +# an explicit [package] lib entry point overrides legacy layouts. +mkdir -p "$WORK/libproj/app" "$WORK/libproj/chosen/src" +cat > "$WORK/libproj/chosen/flint.toml" <<'EOF_LIB' +[package] +name = "chosen" +version = "1.0.0" +lib = "src/api.fl" +EOF_LIB +cat > "$WORK/libproj/chosen/main.fl" <<'EOF_LIB' +export fn value() { return "wrong main" } +EOF_LIB +cat > "$WORK/libproj/chosen/src/api.fl" <<'EOF_LIB' +export fn value() { return "manifest entry" } +EOF_LIB +cat > "$WORK/libproj/app/flint.toml" <<'EOF_LIB' +[package] +name = "lib-app" +version = "0.1.0" + +[dependencies] +chosen = { path = "../chosen" } +EOF_LIB +cat > "$WORK/libproj/app/run.fl" <<'EOF_LIB' +import "chosen" +print(chosen.value()) +EOF_LIB +cd "$WORK/libproj/app" || exit 1 +"$FLINT" pkg install >/dev/null 2>&1 +expect "declared library entry point is used" "manifest entry" "$("$FLINT" run.fl 2>&1)" +expect "declared entry point was copied" "yes" \ + "$([ -f flint_modules/chosen/src/api.fl ] && echo yes || echo no)" +cd "$WORK/pathproj/app" || exit 1 + # reinstall is idempotent "$FLINT" pkg install >/dev/null 2>&1 expect "reinstall keeps the module" "yes" \