Skip to content

Commit a87fcd3

Browse files
Add operator INCLUDE (closes #224)
1 parent 7ead57c commit a87fcd3

25 files changed

Lines changed: 682 additions & 0 deletions

docs/SPECIFICATION.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,8 @@
873873
874874
- `BOOL IMPORT_PATH(STR path)` or `BOOL IMPORT_PATH(STR path, STR alias)` = MUST load a module from an explicit filesystem path and expose it under `alias` or, if omitted, under a basename-derived module name. The argument MUST at minimum accept an absolute path to a `.pre` source file. The module's basename, excluding the `.pre` suffix, MUST be used as the default qualified module name when no alias is supplied. The loaded module MUST otherwise obey the same isolation, caching, and exposure rules as `IMPORT`. Runtime extensions for that module MUST be loaded explicitly via `EXTEND`. On success, `IMPORT_PATH` MUST return `FALSE`.
875875
876+
- `BOOL INCLUDE(STR module)` = MUST load the named module using the same search rules as `IMPORT`, execute it in its own isolated environment on first load, and cache that environment for subsequent calls. After the module loads, its exported bindings MUST be injected directly into the caller's environment under their original unqualified names. Unlike `IMPORT`, `INCLUDE` MUST NOT create a qualified namespace for the module and MUST NOT assign the module name itself as a binding in the caller. Re-importing an already loaded module MUST reuse the cached module namespace rather than re-executing the module. On success, `INCLUDE` MUST return `FALSE`.
877+
876878
- `BOOL EXPORT(STR symbol, STR module)` = MUST copy the caller's current binding for `symbol` into the namespace of the already imported module designated by `module`, making the exported binding available through that module's qualified namespace. `EXPORT` MUST return `FALSE` on success and MUST raise a runtime error if `module` is not currently imported.
877879
878880
---

src/builtins.c

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10046,6 +10046,33 @@ static Value builtin_extend(Interpreter *interp, Value *args, int argc, Expr **a
1004610046
return value_bool(false);
1004710047
}
1004810048

10049+
static int module_include_bindings(Interpreter *interp, Env *caller_env, Env *mod_env) {
10050+
if (!interp || !caller_env || !mod_env) {
10051+
return -1;
10052+
}
10053+
10054+
for (size_t i = 0; i < mod_env->count; i++) {
10055+
EnvEntry *e = &mod_env->entries[i];
10056+
if (!e->initialized) {
10057+
continue;
10058+
}
10059+
if (e->name && e->name[0] == '_' && e->name[1] == '_') {
10060+
continue;
10061+
}
10062+
if (!env_assign(caller_env, e->name, e->value, e->decl_type, e->decl_base, true)) {
10063+
if (interp->error) {
10064+
free(interp->error);
10065+
}
10066+
interp->error = strdup("INCLUDE failed to assign module binding");
10067+
interp->error_line = 0;
10068+
interp->error_col = 0;
10069+
return -1;
10070+
}
10071+
}
10072+
10073+
return 0;
10074+
}
10075+
1004910076
static int module_export_bindings(Interpreter *interp, Env *caller_env, Env *mod_env, const char *alias, int line,
1005010077
int col, const char *fail_msg) {
1005110078
if (!interp || !caller_env || !mod_env || !alias || alias[0] == '\0') {
@@ -10486,6 +10513,283 @@ static Value builtin_import(Interpreter *interp, Value *args, int argc, Expr **a
1048610513
return value_bool(false);
1048710514
}
1048810515

10516+
static Value builtin_include(Interpreter *interp, Value *args, int argc, Expr **arg_nodes, Env *env, int line,
10517+
int col) {
10518+
(void)arg_nodes;
10519+
(void)env;
10520+
if (argc < 1) {
10521+
RUNTIME_ERROR(interp, "INCLUDE expects a module name STR", line, col);
10522+
}
10523+
if (args[0].type != VAL_STR) {
10524+
RUNTIME_ERROR(interp, "INCLUDE first argument must be STR", line, col);
10525+
}
10526+
const char *modname = args[0].as.s ? args[0].as.s : "";
10527+
10528+
const char *referer_source = NULL;
10529+
EnvEntry *src_entry = env_get_entry(env, "__MODULE_SOURCE__");
10530+
if (src_entry && src_entry->initialized && src_entry->value.type == VAL_STR) {
10531+
referer_source = src_entry->value.as.s;
10532+
}
10533+
10534+
char referer_dir[1024] = {0};
10535+
if (referer_source && referer_source[0] != '\0') {
10536+
strncpy(referer_dir, referer_source, sizeof(referer_dir) - 1);
10537+
char *last_sep = NULL;
10538+
for (char *p = referer_dir; *p; p++) {
10539+
if (*p == '/' || *p == '\\') {
10540+
last_sep = p;
10541+
}
10542+
}
10543+
if (last_sep) {
10544+
*last_sep = '\0';
10545+
}
10546+
} else {
10547+
strncpy(referer_dir, ".", sizeof(referer_dir) - 1);
10548+
}
10549+
10550+
#ifdef _WIN32
10551+
const char PATH_SEP = '\\';
10552+
#else
10553+
const char PATH_SEP = '/';
10554+
#endif
10555+
char base[1024];
10556+
base[0] = '\0';
10557+
const char *p = modname;
10558+
char *b = base;
10559+
while (*p && (size_t)(b - base) + 1 < sizeof(base)) {
10560+
if (p[0] == '.' && p[1] == '.') {
10561+
*b++ = PATH_SEP;
10562+
p += 2;
10563+
continue;
10564+
}
10565+
*b++ = *p++;
10566+
}
10567+
*b = '\0';
10568+
10569+
struct stat st;
10570+
char candidate[2048];
10571+
char *found_path = NULL;
10572+
char *srcbuf = NULL;
10573+
10574+
const char *search_dirs[5];
10575+
search_dirs[0] = referer_dir;
10576+
10577+
EnvEntry *primary_src_entry =
10578+
interp && interp->global_env ? env_get_entry(interp->global_env, "__MODULE_SOURCE__") : NULL;
10579+
char primary_program_dir[1024];
10580+
char primary_std_dir[1024];
10581+
char primary_usr_dir[1024];
10582+
primary_program_dir[0] = '\0';
10583+
primary_std_dir[0] = '\0';
10584+
primary_usr_dir[0] = '\0';
10585+
if (primary_src_entry && primary_src_entry->initialized && primary_src_entry->value.type == VAL_STR &&
10586+
primary_src_entry->value.as.s && primary_src_entry->value.as.s[0] != '\0') {
10587+
strncpy(primary_program_dir, primary_src_entry->value.as.s, sizeof(primary_program_dir) - 1);
10588+
primary_program_dir[sizeof(primary_program_dir) - 1] = '\0';
10589+
char *last_sep = NULL;
10590+
for (char *q = primary_program_dir; *q; q++) {
10591+
if (*q == '/' || *q == '\\') {
10592+
last_sep = q;
10593+
}
10594+
}
10595+
if (last_sep) {
10596+
*last_sep = '\0';
10597+
}
10598+
if (snprintf(primary_std_dir, sizeof(primary_std_dir), "%s/lib/std", primary_program_dir) >= 0) {
10599+
search_dirs[1] = primary_std_dir;
10600+
} else {
10601+
search_dirs[1] = "lib/std";
10602+
}
10603+
if (snprintf(primary_usr_dir, sizeof(primary_usr_dir), "%s/lib/usr", primary_program_dir) >= 0) {
10604+
search_dirs[2] = primary_usr_dir;
10605+
} else {
10606+
search_dirs[2] = "lib/usr";
10607+
}
10608+
} else {
10609+
search_dirs[1] = "lib/std";
10610+
search_dirs[2] = "lib/usr";
10611+
}
10612+
10613+
char exe_program_dir[1024];
10614+
char exe_std_dir[1024];
10615+
char exe_usr_dir[1024];
10616+
exe_program_dir[0] = '\0';
10617+
exe_std_dir[0] = '\0';
10618+
exe_usr_dir[0] = '\0';
10619+
if (g_argv && g_argv[0] && g_argv[0][0] != '\0') {
10620+
strncpy(exe_program_dir, g_argv[0], sizeof(exe_program_dir) - 1);
10621+
exe_program_dir[sizeof(exe_program_dir) - 1] = '\0';
10622+
char *last_sep = NULL;
10623+
for (char *q = exe_program_dir; *q; q++) {
10624+
if (*q == '/' || *q == '\\') {
10625+
last_sep = q;
10626+
}
10627+
}
10628+
if (last_sep) {
10629+
*last_sep = '\0';
10630+
}
10631+
if (snprintf(exe_std_dir, sizeof(exe_std_dir), "%s/lib/std", exe_program_dir) >= 0) {
10632+
search_dirs[3] = exe_std_dir;
10633+
} else {
10634+
search_dirs[3] = "lib/std";
10635+
}
10636+
if (snprintf(exe_usr_dir, sizeof(exe_usr_dir), "%s/lib/usr", exe_program_dir) >= 0) {
10637+
search_dirs[4] = exe_usr_dir;
10638+
} else {
10639+
search_dirs[4] = "lib/usr";
10640+
}
10641+
} else {
10642+
search_dirs[3] = "lib/std";
10643+
search_dirs[4] = "lib/usr";
10644+
}
10645+
10646+
for (int sd = 0; sd < 5 && !found_path; sd++) {
10647+
const char *sdir = search_dirs[sd];
10648+
if (!sdir) {
10649+
continue;
10650+
}
10651+
10652+
if (snprintf(candidate, sizeof(candidate), "%s/%s", sdir, base) < 0) {
10653+
continue;
10654+
}
10655+
if (stat(candidate, &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR) {
10656+
char initpath[2048];
10657+
if (snprintf(initpath, sizeof(initpath), "%s/%s/init.pre", sdir, base) < 0) {
10658+
continue;
10659+
}
10660+
if (stat(initpath, &st) == 0 && (st.st_mode & S_IFMT) == S_IFREG) {
10661+
found_path = strdup(initpath);
10662+
break;
10663+
}
10664+
char buf[256];
10665+
snprintf(buf, sizeof(buf), "INCLUDE: package '%s' missing init.pre", modname);
10666+
RUNTIME_ERROR(interp, buf, line, col);
10667+
}
10668+
10669+
char filepath[2048];
10670+
if (snprintf(filepath, sizeof(filepath), "%s/%s.pre", sdir, base) < 0) {
10671+
continue;
10672+
}
10673+
if (stat(filepath, &st) == 0 && (st.st_mode & S_IFMT) == S_IFREG) {
10674+
found_path = strdup(filepath);
10675+
break;
10676+
}
10677+
}
10678+
10679+
char *canonical_path = found_path ? prefix_fullpath_dup(found_path) : NULL;
10680+
const char *cache_key = canonical_path ? canonical_path : modname;
10681+
10682+
if (!found_path) {
10683+
Env *existing = module_env_lookup(interp, cache_key);
10684+
if (!existing) {
10685+
free(found_path);
10686+
free(canonical_path);
10687+
char buf[256];
10688+
snprintf(buf, sizeof(buf), "INCLUDE: module '%s' not found", modname);
10689+
RUNTIME_ERROR(interp, buf, line, col);
10690+
}
10691+
}
10692+
10693+
Env *mod_env = module_env_lookup(interp, cache_key);
10694+
if (!mod_env) {
10695+
mod_env = module_env_lookup(interp, modname);
10696+
}
10697+
if (!mod_env) {
10698+
if (module_register(interp, cache_key) != 0) {
10699+
free(found_path);
10700+
free(canonical_path);
10701+
RUNTIME_ERROR(interp, "INCLUDE failed to register module", line, col);
10702+
}
10703+
mod_env = module_env_lookup(interp, cache_key);
10704+
}
10705+
if (!mod_env) {
10706+
free(found_path);
10707+
free(canonical_path);
10708+
RUNTIME_ERROR(interp, "INCLUDE failed to lookup module env", line, col);
10709+
}
10710+
10711+
if (strcmp(modname, cache_key) != 0) {
10712+
(void)module_register_alias(interp, modname, mod_env);
10713+
}
10714+
if (found_path && strcmp(found_path, cache_key) != 0) {
10715+
(void)module_register_alias(interp, found_path, mod_env);
10716+
}
10717+
10718+
EnvEntry *scope_entry = env_get_entry(mod_env, "__MODULE_SCOPE__");
10719+
if (!scope_entry || !scope_entry->initialized || scope_entry->value.type != VAL_STR) {
10720+
env_assign(mod_env, "__MODULE_SCOPE__", value_str(modname), TYPE_STR, 0, true);
10721+
}
10722+
10723+
EnvEntry *marker = env_get_entry(mod_env, "__MODULE_LOADED__");
10724+
if ((!marker || !marker->initialized) && found_path) {
10725+
FILE *f = fopen(found_path, "rb");
10726+
if (f) {
10727+
fseek(f, 0, SEEK_END);
10728+
long len = ftell(f);
10729+
fseek(f, 0, SEEK_SET);
10730+
srcbuf = malloc((size_t)len + 1);
10731+
if (!srcbuf) {
10732+
fclose(f);
10733+
free(found_path);
10734+
free(canonical_path);
10735+
RUNTIME_ERROR(interp, "Out of memory", line, col);
10736+
}
10737+
if (fread(srcbuf, 1, (size_t)len, f) != (size_t)len) {
10738+
free(srcbuf);
10739+
srcbuf = NULL;
10740+
}
10741+
if (srcbuf) {
10742+
srcbuf[len] = '\0';
10743+
fclose(f);
10744+
10745+
env_assign(mod_env, "__MODULE_SOURCE__", value_str(cache_key), TYPE_STR, 0, true);
10746+
10747+
Lexer lex;
10748+
lexer_init(&lex, srcbuf, found_path);
10749+
Parser parser;
10750+
parser_init(&parser, &lex);
10751+
10752+
Stmt *program = parser_parse(&parser);
10753+
if (parser.had_error) {
10754+
free(srcbuf);
10755+
free(found_path);
10756+
free(canonical_path);
10757+
interp->error = strdup("INCLUDE: parse error");
10758+
interp->error_line = parser.current_token.line;
10759+
interp->error_col = parser.current_token.column;
10760+
return value_null();
10761+
}
10762+
10763+
ExecResult res = exec_program_in_env(interp, program, mod_env);
10764+
if (res.status == EXEC_ERROR) {
10765+
free(srcbuf);
10766+
free(found_path);
10767+
free(canonical_path);
10768+
interp->error = res.error ? strdup(res.error) : strdup("Runtime error in INCLUDE");
10769+
interp->error_line = res.error_line;
10770+
interp->error_col = res.error_column;
10771+
free(res.error);
10772+
return value_null();
10773+
}
10774+
10775+
env_assign(mod_env, "__MODULE_LOADED__", value_int(1), TYPE_INT, 0, true);
10776+
free(srcbuf);
10777+
} else {
10778+
fclose(f);
10779+
}
10780+
}
10781+
}
10782+
10783+
free(found_path);
10784+
free(canonical_path);
10785+
10786+
if (module_include_bindings(interp, env, mod_env) != 0) {
10787+
return value_null();
10788+
}
10789+
10790+
return value_bool(false);
10791+
}
10792+
1048910793
// TNS operator: two forms
1049010794
// 1) TNS(STR: string) -> 1-D TNS of STR single-character elements
1049110795
// 2) TNS(TNS: shape, ANY: value) -> creates tensor with given shape filled with value
@@ -11552,6 +11856,7 @@ static BuiltinFunction builtins_table[] = {
1155211856
{"EXTEND", 1, 1, builtin_extend},
1155311857
{"IMPORT", 1, 2, builtin_import},
1155411858
{"IMPORT_PATH", 1, 2, builtin_import_path},
11859+
{"INCLUDE", 1, 1, builtin_include},
1155511860
{"EXPORT", 2, 2, builtin_export},
1155611861

1155711862
// Sentinel
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE("path", "extra")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE("prefixincludemissingmoduletest")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE(TRUE)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE(0d1.0)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE(LAMBDA BOOL: (){})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE(0d1)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INCLUDE(<>)

0 commit comments

Comments
 (0)