Skip to content

Commit 0a91bec

Browse files
gh-92: Replace .prex with EXTEND.
1 parent 190ad1e commit 0a91bec

File tree

14 files changed

+525
-316
lines changed

14 files changed

+525
-316
lines changed

docs/SPECIFICATION.html

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -664,9 +664,9 @@
664664

665665
---
666666

667-
### 8.2 Extensions and pointer files
667+
### 8.2 Extensions and `EXTEND`
668668

669-
The interpreter MAY accept zero or more extension arguments before the program argument. Extensions MUST be loaded before parsing so that extension-defined type names, operators, and hooks are available during parse and execution.
669+
The interpreter MAY accept zero or more compiled extension-library arguments before the program argument. Those libraries MUST be loaded before parsing so that extension-defined operators and hooks are available during parse and execution.
670670

671671
A compiled extension MUST be a platform-specific dynamic library (`.dll` on Windows, `.so` on Unix-like systems, and `.dylib` on macOS) and MUST export a public initialization function named `prefix_extension_init`. The interpreter MUST call that function with the extension context defined in `prefix_extension.h`, which provides the API version constant together with registration callbacks for operators, custom types, event handlers, periodic hooks, and optional REPL replacement.
672672

@@ -686,9 +686,11 @@
686686

687687
- `register_repl_handler(repl_fn)` to replace or augment the default REPL implementation.
688688

689-
A `.prex` pointer file MUST be treated as a text file containing one extension path per line. Blank lines MUST be ignored, and lines beginning with `!` MUST be treated as comments. Relative paths in a pointer file MUST be resolved relative to the pointer file's directory, with the current working directory and the interpreter's `ext/std/` and `ext/usr/` directories available as fallbacks when necessary. When both bundled and user extension directories are searched, `ext/std/` MUST be consulted before `ext/usr/`.
689+
The interpreter MUST NOT load `.prex` pointer files.
690690

691-
If a `.prex` file is supplied as an argument, the interpreter MUST load all referenced extensions. If no explicit extension arguments are supplied, the interpreter MUST also search for an automatically-discovered pointer file named `.prex` in the current working directory, and when running a source file it MUST also search the program's directory and the companion `<program>.prex` file beside the source file.
691+
Runtime extension loading from Prefix source MUST use `EXTEND(EXTENSION: ext)`, defined in [9.1.8](#918-function-and-module-operators). The `ext` specifier MUST exclude the platform filename suffix (`.dll`, `.so`, `.dylib`) and MAY use package semantics with `..`. When `ext` names a package, the loader MUST attempt `ext..init`.
692+
693+
`EXTEND` MUST resolve extension libraries using the same extension search roots used by compiled-library loading: the calling module directory when available, then current working directory, then interpreter `ext/std`, `ext/usr`, `lib/std`, and `lib/usr` roots with bundled roots consulted before user roots.
692694

693695
---
694696

@@ -726,6 +728,8 @@
726728

727729
`MODULE` is a pseudo-type indicating that the argument MUST be a plain unquoted module identifier or a package-qualified module name using the language's `..` separator. A slash-separated signature such as `ADD/SUB/MUL` denotes a family of distinct operators that share the same argument rules and differ only in the named operation.
728730

731+
`EXTENSION` is a pseudo-type indicating that the argument MUST be a plain unquoted extension specifier used by `EXTEND`, excluding the platform filename suffix and optionally using package-style `..` separators.
732+
729733
`INT` and `FLT` are not interoperable. Unless an operator explicitly permits or requires type mixing, all numeric operands MUST share the same numeric type; supplying mismatched types MUST raise a runtime error. The numeric base of an operation's result MUST be the highest base present among its numeric operands.
730734

731735
Built-in operator names MUST be matched case-sensitively and MUST be written in their canonical form. A user-defined function MUST NOT share a name with any built-in operator; such a conflict MUST raise a runtime error. Extensions MAY register additional operators, which are dispatched through the same call syntax and MAY be qualified with a dotted extension-name prefix, but whose names MUST NOT conflict with those of built-in operators.
@@ -880,17 +884,21 @@
880884
881885
- `INT: MAIN()` = MUST return `1` when the call site is in the primary program source and `0` when the call site is executing from imported module code. The result MUST depend on the source location of the call expression rather than on the dynamic caller stack.
882886
883-
- `IMPORT(MODULE: name)` or `IMPORT(MODULE: name, SYMBOL: alias)` = MUST load the named module, execute it in its own top-level environment on first import, cache that environment, and expose its bindings under the module name or the supplied alias. The implementation MUST search the referring source directory first, then bundled library locations as described in [8.2](#82-extensions-and-pointer-files), [10](#10-standard-library), and the language's module-search rules. Re-importing an already loaded module MUST reuse the cached module namespace rather than re-executing the module.
887+
- `INT: EXTEND(EXTENSION: ext)` = MUST load the compiled extension designated by `ext`. The specifier `ext` MUST exclude the platform filename suffix and MAY use package-style `..` separators. If `ext` resolves to a package, the loader MUST attempt `ext..init`. Relative extension names MUST resolve using the calling module's source directory when available, then the current working directory, then the configured extension roots `ext/std`, `ext/usr`, `lib/std`, and `lib/usr` (with bundled roots searched before user roots). Repeating an `EXTEND` request for an already-loaded extension exposure MUST be idempotent.
888+
889+
Operators registered with module-qualified flags (for example `PREFIX_EXTENSION_ASMODULE`) MUST be exposed only under the extending module's namespace. Importing that module MUST expose the extension namespace qualifier under the importing module's qualified name. Extension side effects outside operator registration (for example process-global hooks or host-side state) remain global.
890+
891+
- `IMPORT(MODULE: name)` or `IMPORT(MODULE: name, SYMBOL: alias)` = MUST load the named module, execute it in its own top-level environment on first import, cache that environment, and expose its bindings under the module name or the supplied alias. The implementation MUST search the referring source directory first, then bundled library locations as described in [8.2](#82-extensions-and-extend), [10](#10-standard-library), and the language's module-search rules. Re-importing an already loaded module MUST reuse the cached module namespace rather than re-executing the module.
884892
885893
Prefix package namespaces MUST use `..` as the package separator. The canonical form is `package..subpackage..module`. When `IMPORT(pkg)` is used and a package directory named `pkg` exists, the interpreter MUST prefer package resolution and attempt to load `pkg/init.pre`. If that package directory exists but contains no `init.pre`, the import MUST raise a runtime error. When `IMPORT(pkg..mod)` is used, the interpreter MUST resolve to `pkg/mod.pre`. If both a package directory and a same-named module file exist in the same search location, the package MUST take precedence.
886894
887895
When the referring source was itself loaded from a file, the search MUST begin in that source file's directory. When the referring source is executing via `-source`, the primary search directory MUST be the current working directory. After local search, the interpreter MUST consult `lib/std/` immediately before `lib/usr/`.
888896
889897
On first import, the module source MUST execute in its own isolated top-level environment. Unqualified identifiers during that execution MUST resolve within the module's own namespace. After execution completes, that namespace MUST be cached and reused by subsequent imports in the same interpreter process. Multiple aliases for the same module MUST provide distinct qualified views into the same cached module instance.
890898
891-
When a module is imported, the interpreter MUST also attempt to load any associated runtime extensions. It MUST first look for a companion pointer file named `<module>.prex` beside the resolved module source. If no such pointer file is present, or as an additional fallback, it MUST also check the configured extension search locations for a same-named extension module. Any extensions loaded this way MUST become available before control returns from the import.
899+
Module import MUST NOT implicitly load companion extension pointer files. Runtime extensions for module code MUST be loaded explicitly via `EXTEND` inside the module.
892900
893-
- `IMPORT_PATH(STR: path)` or `IMPORT_PATH(STR: path, SYMBOL: 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`, including companion extension loading from a sibling `<module>.prex` pointer file when present and same-named extension-module lookup in the configured extension search locations.
901+
- `IMPORT_PATH(STR: path)` or `IMPORT_PATH(STR: path, SYMBOL: 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`.
894902
895903
- `INT: EXPORT(SYMBOL: symbol, MODULE: 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 `0` on success and MUST raise a runtime error if `module` is not currently imported.
896904

lib/std/gui/init.pre

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
! `image`-interoperable GUI operations for Prefix
22

3+
EXTEND(EXTENSION: gui)
4+
35
FUNC INT: SCREEN_WIDTH(){
46
RETURN(gui.SCREEN()[0d1])
57
}

lib/std/gui/init.prex

Lines changed: 0 additions & 1 deletion
This file was deleted.

lib/std/image/init.pre

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
! The returned tensor layout is [column][row][channel] (width, height, channel)
55
! in user code. Channels are ordered r, g, b, a and values are 0..255.
66

7+
EXTEND(EXTENSION: image)
8+
79
IMPORT(path)
810

911
FUNC TNS: LOAD(STR: img_path){

lib/std/image/init.prex

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/builtins.c

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,42 @@ static char* canonicalize_existing_path(const char* path) {
330330
return strdup(path);
331331
}
332332

333+
static char* module_source_dir_dup(Env* env) {
334+
if (!env) return NULL;
335+
EnvEntry* src_entry = env_get_entry(env, "__MODULE_SOURCE__");
336+
if (!src_entry || !src_entry->initialized || src_entry->value.type != VAL_STR || !src_entry->value.as.s) {
337+
return NULL;
338+
}
339+
340+
const char* src = src_entry->value.as.s;
341+
if (src[0] == '\0' || strcmp(src, "<string>") == 0 || strcmp(src, "<repl>") == 0) {
342+
return NULL;
343+
}
344+
345+
char* dir = strdup(src);
346+
if (!dir) return NULL;
347+
348+
char* last_sep = NULL;
349+
for (char* p = dir; *p; p++) {
350+
if (*p == '/' || *p == '\\') last_sep = p;
351+
}
352+
if (last_sep) {
353+
*last_sep = '\0';
354+
} else {
355+
strcpy(dir, ".");
356+
}
357+
return dir;
358+
}
359+
360+
static const char* module_scope_name(Env* env) {
361+
if (!env) return NULL;
362+
EnvEntry* scope_entry = env_get_entry(env, "__MODULE_SCOPE__");
363+
if (!scope_entry || !scope_entry->initialized || scope_entry->value.type != VAL_STR || !scope_entry->value.as.s) {
364+
return NULL;
365+
}
366+
return scope_entry->value.as.s[0] != '\0' ? scope_entry->value.as.s : NULL;
367+
}
368+
333369
// Global argv storage (set by main via builtins_set_argv)
334370
static int g_argc = 0;
335371
static char** g_argv = NULL;
@@ -4847,6 +4883,11 @@ static Value builtin_import_path(Interpreter* interp, Value* args, int argc, Exp
48474883
(void)module_register_alias(interp, alias, mod_env);
48484884
}
48494885

4886+
EnvEntry* scope_entry = env_get_entry(mod_env, "__MODULE_SCOPE__");
4887+
if (!scope_entry || !scope_entry->initialized || scope_entry->value.type != VAL_STR) {
4888+
env_assign(mod_env, "__MODULE_SCOPE__", value_str(alias), TYPE_STR, true);
4889+
}
4890+
48504891
// If not already loaded, execute module source once.
48514892
EnvEntry* marker = env_get_entry(mod_env, "__MODULE_LOADED__");
48524893
if ((!marker || !marker->initialized) && found_path) {
@@ -6817,6 +6858,57 @@ static Value builtin_exit(Interpreter* interp, Value* args, int argc, Expr** arg
68176858
(void)code; // exit does not return
68186859
}
68196860

6861+
static Value builtin_extend(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col) {
6862+
if (argc < 1 || !arg_nodes || !arg_nodes[0]) {
6863+
RUNTIME_ERROR(interp, "EXTEND expects EXTENSION: name", line, col);
6864+
}
6865+
6866+
const char* spec = NULL;
6867+
Expr* spec_node = arg_nodes[0];
6868+
6869+
if (spec_node->type == EXPR_TYPED_IDENT) {
6870+
spec = spec_node->as.typed_ident.name;
6871+
} else if (spec_node->type == EXPR_IDENT) {
6872+
spec = spec_node->as.ident;
6873+
} else if (args && args[0].type == VAL_STR) {
6874+
spec = args[0].as.s;
6875+
}
6876+
6877+
if (!spec || spec[0] == '\0') {
6878+
RUNTIME_ERROR(interp, "EXTEND expects a non-empty extension specifier", line, col);
6879+
}
6880+
6881+
char* base_dir = module_source_dir_dup(env);
6882+
const char* scope_name = module_scope_name(env);
6883+
6884+
char* loaded_name = NULL;
6885+
char* ext_err = NULL;
6886+
int rc = extensions_load_named(spec, base_dir, scope_name, &loaded_name, &ext_err);
6887+
free(base_dir);
6888+
6889+
if (rc != 0) {
6890+
if (ext_err) {
6891+
interp->error = strdup(ext_err);
6892+
free(ext_err);
6893+
} else {
6894+
interp->error = strdup("EXTEND failed to load extension");
6895+
}
6896+
interp->error_line = line;
6897+
interp->error_col = col;
6898+
free(loaded_name);
6899+
return value_null();
6900+
}
6901+
6902+
// Expose the extension namespace symbol in the current module environment.
6903+
if (loaded_name && loaded_name[0] != '\0') {
6904+
(void)env_assign(env, loaded_name, value_str(""), TYPE_STR, true);
6905+
}
6906+
6907+
free(loaded_name);
6908+
free(ext_err);
6909+
return value_int(0);
6910+
}
6911+
68206912
// Stubs for operations requiring TNS/MAP/THD
68216913
static Value builtin_import(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col) {
68226914
(void)args; (void)argc;
@@ -6974,40 +7066,6 @@ static Value builtin_import(Interpreter* interp, Value* args, int argc, Expr** a
69747066
}
69757067
}
69767068

6977-
/* Attempt to load a companion .prex pointer file next to the resolved
6978-
module file so that any extension libraries listed there are available
6979-
during module execution (e.g. lib/std/image/init.prex -> win32.dll). */
6980-
if (found_path) {
6981-
char noext[2048];
6982-
strncpy(noext, found_path, sizeof(noext)-1);
6983-
noext[sizeof(noext)-1] = '\0';
6984-
char* dot = strrchr(noext, '.');
6985-
if (dot) *dot = '\0';
6986-
size_t need = strlen(noext) + strlen(".prex") + 1;
6987-
char* companion_prex = malloc(need);
6988-
if (companion_prex) {
6989-
snprintf(companion_prex, need, "%s.prex", noext);
6990-
char* ext_err = NULL;
6991-
int loaded_any = 0;
6992-
if (extensions_load_prex_if_exists(companion_prex, &loaded_any, &ext_err) != 0) {
6993-
if (ext_err) {
6994-
interp->error = strdup(ext_err);
6995-
free(ext_err);
6996-
} else {
6997-
interp->error = strdup("Failed to load companion .prex");
6998-
}
6999-
interp->error_line = line;
7000-
interp->error_col = col;
7001-
free(companion_prex);
7002-
free(found_path);
7003-
free(canonical_path);
7004-
return value_null();
7005-
}
7006-
free(ext_err);
7007-
free(companion_prex);
7008-
}
7009-
}
7010-
70117069
Env* mod_env = module_env_lookup(interp, cache_key);
70127070
if (!mod_env) mod_env = module_env_lookup(interp, modname);
70137071
if (!mod_env) {
@@ -7031,6 +7089,11 @@ static Value builtin_import(Interpreter* interp, Value* args, int argc, Expr** a
70317089
(void)module_register_alias(interp, found_path, mod_env);
70327090
}
70337091

7092+
EnvEntry* scope_entry = env_get_entry(mod_env, "__MODULE_SCOPE__");
7093+
if (!scope_entry || !scope_entry->initialized || scope_entry->value.type != VAL_STR) {
7094+
env_assign(mod_env, "__MODULE_SCOPE__", value_str(modname), TYPE_STR, true);
7095+
}
7096+
70347097
EnvEntry* marker = env_get_entry(mod_env, "__MODULE_LOADED__");
70357098
if ((!marker || !marker->initialized) && found_path) {
70367099
FILE* f = fopen(found_path, "rb");
@@ -7863,6 +7926,7 @@ static BuiltinFunction builtins_table[] = {
78637926
{"MAIN", 0, 0, builtin_main},
78647927
{"OS", 0, 0, builtin_os},
78657928
{"EXIT", 0, 1, builtin_exit},
7929+
{"EXTEND", 1, 1, builtin_extend},
78667930
{"IMPORT", 1, 2, builtin_import},
78677931
{"IMPORT_PATH", 1, 2, builtin_import_path},
78687932
{"EXPORT", 2, 2, builtin_export},

0 commit comments

Comments
 (0)