Skip to content

Commit e1e0dc7

Browse files
gh-7: Add extension support.
1 parent 6f18462 commit e1e0dc7

File tree

14 files changed

+1095
-66
lines changed

14 files changed

+1095
-66
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
*.exe
1+
# Ignore binaries (both executables and libraries)
2+
*.exe
3+
*.app
4+
*.dll
5+
*.so
6+
*.dylib

build.ps1

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,129 @@
11
<#
2-
build-temp.ps1
3-
Compile the project inside a temporary directory under $env:TEMP (e.g. C:\Windows\Temp), copy the resulting EXE back into this repo, and remove all temporary artifacts.
4-
Requires: run from a Developer Command Prompt for Visual Studio where `cl.exe` is on PATH.
2+
build.ps1
3+
Compiles the interpreter in a temporary directory and copies the resulting EXE
4+
back into this repo. Also discovers extension C sources under ext/ and lib/
5+
and compiles each one into a dynamic library next to its source file.
6+
7+
Requires: run from a Developer Command Prompt for Visual Studio where cl.exe is on PATH.
58
Usage (from Prefix-C folder):
6-
powershell -ExecutionPolicy Bypass -File .\build-temp.ps1
9+
powershell -ExecutionPolicy Bypass -File .\build.ps1
710
#>
811

912
$script = Split-Path -Parent $MyInvocation.MyCommand.Definition
1013
$src = Join-Path $script "src"
14+
$extRoots = @(
15+
(Join-Path $script "ext"),
16+
(Join-Path $script "lib"),
17+
(Join-Path $script "tests")
18+
)
1119

12-
Write-Host "Preparing temp build directory under`$env:TEMP`..."
20+
Write-Host "Preparing temp build directory under`$env:TEMP..."
1321
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
1422
$buildDir = Join-Path $env:TEMP ("prefix-build-$stamp")
1523
New-Item -ItemType Directory -Path $buildDir -Force | Out-Null
16-
1724
Write-Host "Build dir: $buildDir"
1825

19-
# Ensure cl.exe is available
2026
$cl = Get-Command cl.exe -ErrorAction SilentlyContinue
2127
if (-not $cl) {
2228
Write-Error "cl.exe not found. Run this script from a Developer Command Prompt for Visual Studio."
29+
Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
2330
exit 1
2431
}
2532

26-
# Collect .c files
2733
$cFiles = Get-ChildItem -Path $src -Filter *.c -File -Recurse | ForEach-Object { $_.FullName }
2834
if ($cFiles.Count -eq 0) {
2935
Write-Error "No .c files found in '$src'"
3036
Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
3137
exit 1
3238
}
3339

34-
Push-Location $buildDir
40+
$platform = [System.Runtime.InteropServices.RuntimeInformation]
41+
$extSuffix = ".dll"
42+
if ($platform::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) {
43+
$extSuffix = ".so"
44+
} elseif ($platform::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX)) {
45+
$extSuffix = ".dylib"
46+
}
3547

48+
Push-Location $buildDir
3649
try {
37-
# Build in temp dir; cl will place outputs in the current dir
38-
$fe = "/Fe:prefix.exe"
39-
$args = @("/std:c17", "/Gd", "/O2", "/Gy", "/GF", "/GL", "/W4", "/WX", "/MP", "/nologo", $fe)
40-
$args += $cFiles
41-
42-
Write-Host "Invoking: cl.exe $($args -join ' ')"
43-
& cl.exe @args
44-
$rc = $LASTEXITCODE
45-
if ($rc -ne 0) {
46-
throw "cl.exe returned exit code $rc"
50+
$exeArgs = @(
51+
"/std:c17", "/Gd", "/O2", "/Gy", "/GF", "/GL", "/W4", "/WX", "/MP", "/nologo",
52+
"/Fe:prefix.exe"
53+
)
54+
$exeArgs += $cFiles
55+
56+
Write-Host "Invoking: cl.exe $($exeArgs -join ' ')"
57+
& cl.exe @exeArgs
58+
if ($LASTEXITCODE -ne 0) {
59+
throw "cl.exe returned exit code $LASTEXITCODE while building interpreter"
4760
}
4861

49-
# Copy EXE back to repo (Prefix-C root)
50-
$outExe = Join-Path $buildDir 'prefix.exe'
62+
$outExe = Join-Path $buildDir "prefix.exe"
5163
if (-not (Test-Path $outExe)) {
5264
throw "Expected output EXE not found: $outExe"
5365
}
54-
$dest = Join-Path $script 'prefix.exe'
55-
Copy-Item -Path $outExe -Destination $dest -Force
56-
Write-Host "Copied EXE to: $dest"
5766

67+
$exeDest = Join-Path $script "prefix.exe"
68+
Copy-Item -Path $outExe -Destination $exeDest -Force
69+
Write-Host "Copied EXE to: $exeDest"
70+
71+
$extSources = @()
72+
foreach ($root in $extRoots) {
73+
if (Test-Path $root) {
74+
$extSources += Get-ChildItem -Path $root -Filter *.c -File -Recurse
75+
}
76+
}
77+
78+
if ($extSources.Count -eq 0) {
79+
Write-Host "No extension C sources found under ext/ or lib/."
80+
} else {
81+
Write-Host "Found $($extSources.Count) extension source file(s)."
82+
}
83+
84+
foreach ($extSource in $extSources) {
85+
$extSourcePath = $extSource.FullName
86+
$extName = [System.IO.Path]::GetFileNameWithoutExtension($extSourcePath)
87+
$extOutName = "$extName$extSuffix"
88+
$extDest = Join-Path $extSource.DirectoryName $extOutName
89+
$extBuildDir = Join-Path $buildDir ("ext-" + [guid]::NewGuid().ToString("N"))
90+
91+
New-Item -ItemType Directory -Path $extBuildDir -Force | Out-Null
92+
Push-Location $extBuildDir
93+
try {
94+
$extArgs = @(
95+
"/std:c17", "/Gd", "/O2", "/W4", "/WX", "/nologo", "/LD",
96+
"/I$src",
97+
"/Fe:$extOutName",
98+
$extSourcePath
99+
)
100+
101+
Write-Host "Invoking: cl.exe $($extArgs -join ' ')"
102+
& cl.exe @extArgs
103+
if ($LASTEXITCODE -ne 0) {
104+
throw "cl.exe returned exit code $LASTEXITCODE while building extension '$extSourcePath'"
105+
}
106+
107+
$extOutPath = Join-Path $extBuildDir $extOutName
108+
if (-not (Test-Path $extOutPath)) {
109+
throw "Expected output extension not found: $extOutPath"
110+
}
111+
112+
Copy-Item -Path $extOutPath -Destination $extDest -Force
113+
Write-Host "Copied extension to: $extDest"
114+
} finally {
115+
Pop-Location
116+
Remove-Item -Recurse -Force $extBuildDir -ErrorAction SilentlyContinue
117+
}
118+
}
58119
} catch {
59120
Write-Error "Build failed: $_"
60-
Pop-Location
61-
# Cleanup temp build dir on failure as well
62-
Write-Host "Cleaning up temp build dir: $buildDir"
63-
Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
64121
exit 1
65122
} finally {
66123
Pop-Location
124+
Write-Host "Cleaning up temp build dir: $buildDir"
125+
Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
67126
}
68127

69-
# Cleanup temp build dir
70-
Write-Host "Cleaning up temp build dir: $buildDir"
71-
Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
72-
73128
Write-Host "Build succeeded and exe copied to: $(Join-Path $script 'prefix.exe')"
74129
exit 0

docs/SPECIFICATION.html

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -416,27 +416,34 @@
416416

417417
- Program argument: the interpreter reads a single *program* argument as its input program. If the `-source` flag is not present, the program argument is interpreted as a path to a source file and the interpreter loads and parses that file. If the `-source` flag is present, the program argument is treated as the source text itself (a string containing the program) and is parsed directly without reading a file.
418418

419-
- Extensions: the interpreter may also accept zero or more *extension* arguments that load Python extension modules before parsing and execution. Extensions may add new operators, new runtime types, and runtime hooks (including custom REPL implementations), but MUST NOT replace or modify existing built-in operators or types.
419+
- Extensions: the interpreter may also accept zero or more *extension* arguments that load compiled extension modules before parsing and execution. Extensions may add new operators, new runtime types, and runtime hooks (including custom REPL implementations), but MUST NOT replace or modify existing built-in operators or types.
420420

421-
- A Python extension is a `.py` file that defines `PREFIX_EXTENSION_API_VERSION = 1` (optional; defaults to 1) and a callable `prefix_register(ext)` entrypoint.
421+
- A compiled extension is a platform-specific dynamically-linked library (`.dll` on Windows, `.so` on Unix/Linux, `.dylib` on macOS) built from C code. Extensions must define a single public C function with C calling convention: `void prefix_extension_init(struct prefix_ext_context *ctx)` that the interpreter invokes at load time to register operators, types, and hooks.
422+
423+
- Extension API: Extensions include the public header `prefix_extension.h` which defines the extension API, including:
424+
- `PREFIX_EXTENSION_API_VERSION`: a version constant that the extension must match (currently 1).
425+
- Registration function pointers (supplied in `prefix_ext_context`) for registering operators: `register_operator(const char *name, prefix_operator_fn fn, int asmodule)`.
426+
- Registration for type constructors and custom type hooks.
427+
- Registration for event handlers: `on_event(const char *event_name, prefix_event_fn fn)` for events such as `program_start`, `program_end`, `on_error`, `before_statement`, `after_statement`, `before_call`, and `after_call`.
428+
- Utilities for allocating and manipulating Prefix runtime values (`prefix_value_t`).
422429

423430
- A pointer file is a `.prex` text file containing one extension path per line. Lines are trimmed; blank lines are ignored; lines beginning with `!` are comments. Relative paths are resolved relative to the `.prex` file's directory; when a referenced path is not found there the interpreter will also try the current working directory and, as a final fallback, the interpreter's own `ext/` subdirectory.
424431

425432
- If a `.prex` file is supplied as an argument, all of the linked extensions are loaded.
426433

427434
- If no explicit extension arguments are provided, the interpreter will automatically look for a pointer file named `.prex` in the current working directory. When a program path is being executed (not when `-source` is used), the interpreter also checks the program's directory for a `.prex` pointer file and will additionally accept a pointer file that shares the program's basename but ends with `.prex` (for example `program.pre` alongside `program.prex`). If any pointer file is found the extensions listed in that pointer file are loaded as if supplied on the command line.
428435

429-
- Extensions are loaded before parsing so that extension-defined type names are recognized in typed assignments and function signatures.
436+
- Extensions are loaded before parsing so that extension-defined type names are recognized in typed assignments and function signatures. At load time, the interpreter uses platform-specific mechanisms (`LoadLibraryEx` on Windows, `dlopen` on Unix-like systems) to load the extension library, locates the `prefix_extension_init` symbol, invokes it with a context pointer, and records all registered operators and hooks.
430437

431438
- If the only supplied positional inputs are extensions (and no program is supplied), the interpreter runs the REPL with the loaded extensions.
432439

433-
- Hook surfaces exposed by the reference implementation include:
440+
- Hook surfaces and registration: Extensions register operators and hooks by calling registration functions provided in the `prefix_ext_context` structure. Supported operations include:
434441

435-
- Operators: additional call names dispatched like built-ins. Extensions may opt into module-qualifying all of their operators by defining the module-level flag `PREFIX_EXTENSION_ASMODULE = True`. When an extension sets this flag every operator it registers will be exposed under the extension's name as a dotted prefix (for example, an extension named `mymod` registering `FOO` will expose `mymod.FOO`). This mirrors imported module-qualified bindings and avoids global name collisions.
442+
- Operators: Extensions call `register_operator(name, handler_fn, flags)` to add new operators dispatched like built-ins. The `flags` parameter includes `PREFIX_EXTENSION_ASMODULE` to opt into module-qualifying all of an extension's operators with the extension's name as a dotted prefix (for example, an extension named `mymod` registering `FOO` will expose `mymod.FOO`). This mirrors imported module-qualified bindings and avoids global name collisions.
436443

437-
- Per-N-steps rules: `every_n_steps(N, handler)` runs the handler after state-log step indices where `step_index % N == 0`.
444+
- Per-N-steps rules: `register_periodic_hook(N, handler_fn)` registers a handler to run after state-log step indices where `step_index % N == 0`.
438445

439-
- Event-bound rules: `on_event(name, handler)` for the following event names:
446+
- Event-bound rules: `register_event_handler(event_name, handler_fn)` for the following event names:
440447

441448
- `program_start(interpreter, program, env)`
442449

@@ -448,7 +455,7 @@
448455

449456
- `before_call(interpreter, name, args, env, location)` / `after_call(interpreter, name, result, env, location)`
450457

451-
- REPL: extensions may provide a replacement REPL implementation.
458+
- REPL: Extensions may register a custom REPL implementation via `register_repl_handler(repl_fn)`.
452459

453460
- Verbose tracebacks: if the `-verbose` flag is supplied on the command line, tracebacks include the environment snapshots described in Section 10.8. In concise traceback mode the `-verbose` flag causes the interpreter to attach an `env_snapshot` entry to each frame shown; in verbose traceback mode the same flag expands the printed `State snapshot: blocks to include the selected local environment and any small set of globals included by policy. The snapshot contents follow the requirements in Section 10.2 and are suitable for deterministic replay.
454461

@@ -464,15 +471,15 @@
464471

465472
- Source-string mode: `prefix -source "foo = INPUT\nPRINT(foo)" -verbose`
466473

467-
- With extensions (file mode): `prefix myext.py program.pre`
474+
- With compiled extension (file mode): `prefix myext.dll program.pre` (Windows) or `prefix myext.so program.pre` (Unix/Linux)
468475

469476
- With pointer file: `prefix extensions.prex program.pre`
470477

471-
- REPL / Interactive mode: `prefix` (no program argument), or `prefix myext.py` (extensions only)
478+
- REPL / Interactive mode: `prefix` (no program argument), or `prefix myext.dll` (extensions only)
472479

473480
## 11. REPL (Interactive Mode)
474481

475-
When the interpreter is invoked without a program path or a `-source` string argument it enters an interactive readevalprint loop (REPL). The REPL is a convenient development and exploration environment that executes Prefix statements using the same parser, runtime, built-ins, and state-logging semantics as file-mode execution. The following rules describe REPL behaviour:
482+
When the interpreter is invoked without a program path or a `-source` string argument it enters an interactive read-eval-print loop (REPL). The REPL is a convenient development and exploration environment that executes Prefix statements using the same parser, runtime, built-ins, and state-logging semantics as file-mode execution. The following rules describe REPL behaviour:
476483

477484
- Invocation: running `prefix` with no program argument launches the REPL. If one or more extensions are supplied and no program argument is supplied, the interpreter launches a REPL with those extensions loaded; if an extension provides a replacement REPL, that REPL may be used.
478485

src/builtins.c

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6875,11 +6875,14 @@ static BuiltinFunction builtins_table[] = {
68756875
{NULL, 0, 0, NULL}
68766876
};
68776877

6878-
void builtins_init(void) {
6879-
// Nothing to initialize for now, table is static
6880-
}
6878+
typedef struct DynamicBuiltin {
6879+
BuiltinFunction fn;
6880+
struct DynamicBuiltin* next;
6881+
} DynamicBuiltin;
68816882

6882-
BuiltinFunction* builtin_lookup(const char* name) {
6883+
static DynamicBuiltin* g_dynamic_builtins = NULL;
6884+
6885+
static BuiltinFunction* builtin_lookup_static(const char* name) {
68836886
for (int i = 0; builtins_table[i].name != NULL; i++) {
68846887
if (strcmp(builtins_table[i].name, name) == 0) {
68856888
return &builtins_table[i];
@@ -6888,6 +6891,95 @@ BuiltinFunction* builtin_lookup(const char* name) {
68886891
return NULL;
68896892
}
68906893

6894+
static BuiltinFunction* builtin_lookup_dynamic(const char* name) {
6895+
for (DynamicBuiltin* n = g_dynamic_builtins; n != NULL; n = n->next) {
6896+
if (n->fn.name && strcmp(n->fn.name, name) == 0) {
6897+
return &n->fn;
6898+
}
6899+
}
6900+
return NULL;
6901+
}
6902+
6903+
void builtins_reset_dynamic(void) {
6904+
DynamicBuiltin* n = g_dynamic_builtins;
6905+
while (n) {
6906+
DynamicBuiltin* next = n->next;
6907+
free((char*)n->fn.name);
6908+
if (n->fn.param_names) {
6909+
for (int i = 0; i < n->fn.param_count; i++) {
6910+
free((char*)n->fn.param_names[i]);
6911+
}
6912+
free((void*)n->fn.param_names);
6913+
}
6914+
free(n);
6915+
n = next;
6916+
}
6917+
g_dynamic_builtins = NULL;
6918+
}
6919+
6920+
int builtins_register_operator(const char* name, BuiltinImplFn impl, int min_args, int max_args, const char** param_names, int param_count) {
6921+
if (!name || name[0] == '\0' || !impl) return -1;
6922+
if (min_args < 0) return -1;
6923+
if (max_args >= 0 && max_args < min_args) return -1;
6924+
if (builtin_lookup_static(name) || builtin_lookup_dynamic(name)) {
6925+
return -1;
6926+
}
6927+
6928+
DynamicBuiltin* node = calloc(1, sizeof(DynamicBuiltin));
6929+
if (!node) return -1;
6930+
6931+
node->fn.name = strdup(name);
6932+
node->fn.min_args = min_args;
6933+
node->fn.max_args = max_args;
6934+
node->fn.impl = impl;
6935+
node->fn.param_names = NULL;
6936+
node->fn.param_count = 0;
6937+
6938+
if (!node->fn.name) {
6939+
free(node);
6940+
return -1;
6941+
}
6942+
6943+
if (param_names && param_count > 0) {
6944+
const char** copy_names = calloc((size_t)param_count, sizeof(char*));
6945+
if (!copy_names) {
6946+
free((char*)node->fn.name);
6947+
free(node);
6948+
return -1;
6949+
}
6950+
for (int i = 0; i < param_count; i++) {
6951+
if (!param_names[i]) {
6952+
copy_names[i] = strdup("");
6953+
} else {
6954+
copy_names[i] = strdup(param_names[i]);
6955+
}
6956+
if (!copy_names[i]) {
6957+
for (int j = 0; j < i; j++) free((char*)copy_names[j]);
6958+
free((void*)copy_names);
6959+
free((char*)node->fn.name);
6960+
free(node);
6961+
return -1;
6962+
}
6963+
}
6964+
node->fn.param_names = copy_names;
6965+
node->fn.param_count = param_count;
6966+
}
6967+
6968+
node->next = g_dynamic_builtins;
6969+
g_dynamic_builtins = node;
6970+
return 0;
6971+
}
6972+
6973+
void builtins_init(void) {
6974+
// Nothing to initialize for now, table is static
6975+
}
6976+
6977+
BuiltinFunction* builtin_lookup(const char* name) {
6978+
BuiltinFunction* b = builtin_lookup_static(name);
6979+
if (b) return b;
6980+
return builtin_lookup_dynamic(name);
6981+
}
6982+
68916983
bool is_builtin(const char* name) {
68926984
return builtin_lookup(name) != NULL;
68936985
}

src/builtins.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
// Forward declaration
99
typedef struct Interpreter Interpreter;
1010

11+
typedef Value (*BuiltinImplFn)(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col);
12+
1113
typedef struct {
1214
const char* name;
1315
int min_args;
1416
int max_args; // -1 for variadic
15-
Value (*impl)(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col);
17+
BuiltinImplFn impl;
1618
// Optional: parameter names for keyword argument binding.
1719
// If NULL/0, the builtin does not accept keyword arguments.
1820
const char** param_names;
@@ -31,4 +33,11 @@ BuiltinFunction* builtin_lookup(const char* name);
3133
// Check if a name is a builtin
3234
bool is_builtin(const char* name);
3335

36+
// Register a runtime builtin operator (used by extensions).
37+
// Returns 0 on success, -1 on failure (duplicate/invalid input/oom).
38+
int builtins_register_operator(const char* name, BuiltinImplFn impl, int min_args, int max_args, const char** param_names, int param_count);
39+
40+
// Remove all runtime-registered operators.
41+
void builtins_reset_dynamic(void);
42+
3443
#endif // BUILTINS_H

0 commit comments

Comments
 (0)