Minor API change dealing with script exiting.
This commit is contained in:
parent
fe0fe73884
commit
86e3a1d71a
9 changed files with 54 additions and 91 deletions
3
API.md
3
API.md
|
|
@ -18,8 +18,6 @@ etc. -- see the README and `src/calog.h`.)
|
|||
it never runs), and every other live script stops at its next native call. So the first
|
||||
`calogExit` decides the process's status -- a `calogExit(1)` on a failed check cannot be
|
||||
followed by more work, nor overwritten by a later `calogExit(0)`.
|
||||
`calogEnd([code])` ends **only the calling script**, the same way: it does not return, its context
|
||||
is reaped, and the other scripts carry on. When the last one ends, `calog` exits.
|
||||
- **Values.** Arguments and results marshal through one canonical type: `nil`, `bool`, `int`,
|
||||
`real`, `string`, `list`, and `map` (keyed record). Strings are **binary-safe** (may contain
|
||||
embedded NULs) everywhere the underlying library allows it.
|
||||
|
|
@ -113,7 +111,6 @@ egress (JavaScript, Berry, s7, mruby); Janet loses `symbol`/`keyword` subtype (i
|
|||
| Function | Description |
|
||||
|---|---|
|
||||
| `calogPrint(...values: any)` | Write each argument to stdout, space-separated, with a trailing newline. |
|
||||
| `calogEnd([code: int])` | End **this script only**, leaving every other script running. **Does not return** -- it unwinds the calling script at the call site. Its context is then reaped, and once every launched script has ended, `calog` exits. A `code` given here names the process's status like `calogExit`'s does (first request wins); omitting it claims nothing. |
|
||||
| `calogExit([code: int])` | Tear down the runtime and exit the process with `code` (default `0`). **Does not return** -- it unwinds the calling script at the call site, so nothing after it runs, and stops every other live script at its next native call. The first caller's `code` is the one reported; a script that catches the unwind still cannot call another native. |
|
||||
|
||||
## archive
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ weeks. `make cross` runs the real thing; `make cross-smoke`, `cross-win`, `cross
|
|||
|
||||
### Re-verified 2026-07-24 (zig 0.16.0)
|
||||
|
||||
The cross-builds were re-run after the teardown/`calogEnd` work. **The core changes port cleanly --
|
||||
The cross-builds were re-run after the teardown work. **The core changes port cleanly --
|
||||
`src/context.c`, `src/value.c` and `src/broker.c` compile, link and run on every target** -- but the
|
||||
run exposed three problems that all predate that work. All three are now fixed:
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ run exposed three problems that all predate that work. All three are now fixed:
|
|||
| `build/cross/mac-arm64/calog` | Mach-O 64-bit arm64, PIE |
|
||||
|
||||
Each links all ten engines and every library, and each carries this session's runtime changes
|
||||
(`calogEnd` and the abort message are in the binaries). Still build-verified only -- running them
|
||||
(the abort message is in the binaries). Still build-verified only -- running them
|
||||
needs a Windows/wine or Mac host.
|
||||
|
||||
The bullets below record the state at the time each port was done; where they disagree with the
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -150,10 +150,10 @@ bin/calog producer.js consumer.lua # several files share one runtime (kv, pubsu
|
|||
```
|
||||
|
||||
Scripts print with `calogPrint(...)` and stop by calling `calogExit([code])`, which tears
|
||||
everything down, or `calogEnd([code])`, which ends only the calling script and lets the others
|
||||
run on (calog is event-driven, so a script asks to stop; `Ctrl-C` also works). Neither
|
||||
returns -- the statement after it never runs, so a build script's `calogExit(1)` is final --
|
||||
and once every script has ended, `calog` exits on its own. [`API.md`](API.md) documents
|
||||
everything down (calog is event-driven, so a script asks to stop; `Ctrl-C` also works). It does
|
||||
not return -- the statement after it never runs, so a build script's `calogExit(1)` is final. A
|
||||
script can also end just itself with `taskExit()`, leaving its siblings running; once every
|
||||
script has ended, `calog` exits on its own. [`API.md`](API.md) documents
|
||||
every native a script can call; [`examples/scripts/`](examples/scripts/) has runnable
|
||||
examples across every engine and library, plus polyglot and multi-file demos.
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ uint64_t calogContextId(const CalogContextT *);
|
|||
uint64_t calogCurrentId(void); // 0 on the host thread
|
||||
CalogT *calogCurrent(void);
|
||||
|
||||
// stopping scripts (what bin/calog's calogExit and calogEnd are built on)
|
||||
// stopping scripts (what bin/calog's calogExit is built on)
|
||||
int32_t calogAbortAll(CalogT *, CalogValueT *result); // return this from a native: every script stops
|
||||
int32_t calogAbortCurrent(CalogValueT *result); // ...or only the calling one, which then retires
|
||||
bool calogAborting(CalogT *); // true once the caller has been stopped
|
||||
|
|
|
|||
43
design.md
43
design.md
|
|
@ -1510,7 +1510,7 @@ handler, that the runtime stays latched, and that a later `calogCall` is refused
|
|||
|
||||
---
|
||||
|
||||
## 26. Reclaiming a context's callables -- and `calogEnd`
|
||||
## 26. Reclaiming a context's callables -- and ending one script
|
||||
|
||||
A `CalogFnT` is a handle to a function living inside one VM. Section 25 made `calogExit` unwind
|
||||
scripts; this section fixes the class of bug that made the *next* question -- "how does one script
|
||||
|
|
@ -1532,7 +1532,7 @@ Three separate holders reproduce it, all deterministically:
|
|||
|
||||
| holder | reproducer (JavaScript, because QuickJS is the VM that checks) |
|
||||
|---|---|
|
||||
| a library registry | `psSubscribe('t', function(){});` then exit, error, or `calogEnd` |
|
||||
| a library registry | `psSubscribe('t', function(){});` then exit, error, or `taskExit` |
|
||||
| another engine | `calogCall('keep', function(){})` into a Lua script that stores it, then die |
|
||||
| an in-flight invoke | a delivery holding the last reference across the owner's teardown |
|
||||
|
||||
|
|
@ -1540,7 +1540,7 @@ The first instinct -- move the pubsub and export destroy hooks from `calogDestro
|
|||
`calogDestroyBeforeContextsE`, as `calogTimer` already does -- fixes only the whole-runtime teardown
|
||||
path, and only for the last runtime in the process. It does nothing for a context that dies while the
|
||||
runtime lives on, which is the common case: a script that errors after subscribing, `taskExit`, and
|
||||
now `calogEnd`. It also cannot reach a value another VM is holding, where no registry is involved.
|
||||
or `taskExit`. It also cannot reach a value another VM is holding, where no registry is involved.
|
||||
|
||||
### The fix: the owner reclaims, on its own thread
|
||||
|
||||
|
|
@ -1633,37 +1633,48 @@ and freed moments later by the teardown that already owns it.
|
|||
runtime -- clean under ASan across repeated runs, and under ThreadSanitizer with the Lua and
|
||||
JavaScript engines linked.
|
||||
|
||||
### `calogEnd([code])` -- ending one script
|
||||
### Ending one script -- `calogAbortCurrent`, and the runner native that was not needed
|
||||
|
||||
With reclamation in place a script can safely end itself, which is what the runner's new `calogEnd`
|
||||
does. It is `calogAbortCurrent`: the same unwind as `calogAbortAll`, scoped to one context. The
|
||||
calling script stops at the call, that context alone is latched (so a caught unwind cannot call
|
||||
another native), and the context retires -- its thread ends, and the runner's pump loop closes it,
|
||||
drops the live count, and exits once the last launched script is gone. Other scripts are untouched.
|
||||
With reclamation in place a context can safely end itself, so the core gained `calogAbortCurrent`:
|
||||
the same unwind as `calogAbortAll`, scoped to one context. The calling script stops at the call, that
|
||||
context alone is latched (so a caught unwind cannot call another native), and the context retires.
|
||||
|
||||
No engine adapter changed for any of this. The ten of them ask one question -- `calogAborting`
|
||||
(sec 25) -- and that question was widened rather than duplicated: it now answers *has the caller been
|
||||
stopped*, by the runtime latch or by its own context. A second query would have meant a second edit
|
||||
to ten files and two ways for them to disagree.
|
||||
|
||||
An optional `code` names the process's status on the same first-writer-wins rule as `calogExit`;
|
||||
omitting it claims nothing. The three ways a script can stop now read as one set:
|
||||
The runner briefly exposed this as a script native, `calogEnd([code])`, and it was **removed again**
|
||||
after measuring what it actually added. Two things had been conflated:
|
||||
|
||||
- **Reaping a self-ended script** -- the thing that was actually missing -- is a RUNNER fix, not a
|
||||
new native. The pump loop only closed contexts flagged `failed`, so a context that retired itself
|
||||
sat in `gLaunched` with `liveCount` never dropping and the runner never exiting. Closing any
|
||||
context whose thread has finished is what fixed that, and `taskExit` (which has always retired the
|
||||
calling context) inherits it for free: a lone script ending with `taskExit()` now exits the run.
|
||||
- **What was left** once reaping worked was immediacy plus an optional exit code. `error(...)`
|
||||
already ends one script with a failing status while its siblings continue, and `calogExit(code)`
|
||||
names the status when you want everything to stop -- leaving `calogEnd` a third verb for the
|
||||
narrow slice of "a specific non-zero status from one script while the others carry on".
|
||||
|
||||
So the runner keeps two verbs and the library keeps its own:
|
||||
|
||||
| | ends | process status |
|
||||
|---|---|---|
|
||||
| `calogEnd([code])` | this script | `code` if given, else unclaimed |
|
||||
| `taskExit()` | this script (deferred -- the current chunk finishes) | unclaimed |
|
||||
| `error(...)` | this script | 1 (the run did not fully succeed) |
|
||||
| `calogExit([code])` | everything | `code` (default 0) |
|
||||
| `calogExit([code])` | everything, immediately | `code` (default 0) |
|
||||
|
||||
`taskExit()` is deliberately left alone: it stays deferred, as documented, and remains the
|
||||
task-scoped way for a spawned task to retire itself.
|
||||
`calogAbortCurrent` stays in the public API: it is the counterpart to `calogAbortAll`, it is what an
|
||||
embedder registers their own per-script "stop" over (the runner is only one consumer of this
|
||||
library), and `tests/testTeardown.c` drives the early-context-death reclaim path through it.
|
||||
|
||||
### Test
|
||||
|
||||
`tests/testTeardown.c` uses JavaScript throughout, because on any other engine a stranded handle is
|
||||
invisible -- there, surviving the teardown IS the assertion. It covers a subscriber, an export and a
|
||||
timer callback left registered at `calogDestroy`; each of those whose script instead errors out
|
||||
first; a script that ends itself with the `calogEnd` primitive; a closure handed to a Lua script
|
||||
first; a script that ends itself with `calogAbortCurrent`; a closure handed to a Lua script
|
||||
whose owner then dies (invoking it afterwards must fail cleanly, not reach into a destroyed VM); and
|
||||
the drained-registry guards.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ Conventions every example follows:
|
|||
script asks to exit explicitly. `Ctrl-C` also tears things down cleanly. `calogExit` does
|
||||
not return: the statement after it never runs, and the first code asked for is the one the
|
||||
process reports.
|
||||
- **`calogEnd([code])`** ends just the calling script, leaving the others running; it does not
|
||||
return either, and its context is reaped. Once every script has ended, `calog` exits on its own.
|
||||
- Extensions map to engines: `.lua .js .nut .bas .be .scm .wren`.
|
||||
|
||||
## `languages/` -- one guided tour per engine
|
||||
|
|
|
|||
|
|
@ -271,8 +271,8 @@ bool calogCurrentShuttingDown(void); // true once the calling context
|
|||
// a latched runtime never runs script code again, so latch it only when tearing the runtime down. The
|
||||
// host decides what happens next -- bin/calog exits the process with the code the script asked for.
|
||||
int32_t calogAbortAll(CalogT *calog, CalogValueT *result);
|
||||
// Stop only the CALLING script, leaving the runtime and every other script running (the runner's
|
||||
// calogEnd). Used the same way -- call it from a native and return its value, and the script unwinds
|
||||
// Stop only the CALLING script, leaving the runtime and every other script running. Used the same
|
||||
// way -- call it from a native and return its value, and the script unwinds
|
||||
// at the call site. The context also retires itself, so its thread ends and the host can reap it;
|
||||
// like an aborted script it is not reported as a failure. Fails if there is no calling script.
|
||||
int32_t calogAbortCurrent(CalogValueT *result);
|
||||
|
|
|
|||
|
|
@ -82,10 +82,8 @@ static BOOL WINAPI consoleHandler(DWORD ctrlType);
|
|||
#endif
|
||||
static const CalogEngineT *engineForExtension(const char *ext);
|
||||
static const char *extensionOf(const char *arg);
|
||||
static int32_t nativeCalogEnd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||
static bool readExitCode(CalogValueT *args, int32_t argCount, CalogValueT *result, int32_t *outCode, bool *outGiven, const char *who);
|
||||
static void noteExitCode(int32_t code);
|
||||
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||
static void onSignal(int sig);
|
||||
|
|
@ -211,27 +209,6 @@ static const char *extensionOf(const char *arg) {
|
|||
}
|
||||
|
||||
|
||||
// calogEnd([code]) -- a script says IT is done, without ending the others. Inline for the same
|
||||
// reason calogExit is: only a native running on the script's own thread can unwind that script.
|
||||
// Does not return. Its context retires, so the runner reaps it; when the last launched script has
|
||||
// ended, the runner exits. A code given here names the process's status the same way calogExit's
|
||||
// does (first request wins); omitting it claims nothing, leaving the status to whatever else happens.
|
||||
static int32_t nativeCalogEnd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||
int32_t code;
|
||||
bool given;
|
||||
|
||||
(void)userData;
|
||||
calogValueNil(result);
|
||||
if (!readExitCode(args, argCount, result, &code, &given, "calogEnd")) {
|
||||
return calogErrArgE;
|
||||
}
|
||||
if (given) {
|
||||
noteExitCode(code);
|
||||
}
|
||||
return calogAbortCurrent(result);
|
||||
}
|
||||
|
||||
|
||||
// calogExit([code]) -- a script asks the runner to tear everything down and exit. Registered inline
|
||||
// so it runs on the CALLING script's thread: that is what lets calogAbortAll unwind this very script
|
||||
// (an error raised on the host thread could not), and everything it touches is thread-safe anyway.
|
||||
|
|
@ -239,12 +216,18 @@ static int32_t nativeCalogEnd(CalogValueT *args, int32_t argCount, CalogValueT *
|
|||
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||
CalogT *calog;
|
||||
int32_t code;
|
||||
bool given;
|
||||
|
||||
(void)userData;
|
||||
calogValueNil(result);
|
||||
if (!readExitCode(args, argCount, result, &code, &given, "calogExit")) {
|
||||
return calogErrArgE;
|
||||
code = 0;
|
||||
if (argCount >= 1) {
|
||||
if (args[0].type == calogIntE) {
|
||||
code = (int32_t)args[0].as.i;
|
||||
} else if (args[0].type == calogRealE) {
|
||||
code = (int32_t)args[0].as.r;
|
||||
} else {
|
||||
return calogFail(result, calogErrArgE, "calogExit expects an optional integer exit code");
|
||||
}
|
||||
}
|
||||
requestShutdown(code);
|
||||
calog = calogCurrent();
|
||||
|
|
@ -417,31 +400,6 @@ static char *readFile(const char *path) {
|
|||
}
|
||||
|
||||
|
||||
// Read the optional exit code shared by calogEnd and calogExit: *outCode is the code (0 when the
|
||||
// script named none) and *outGiven says whether it named one at all. Returns false having written
|
||||
// the diagnostic into result, so the caller returns calogErrArgE.
|
||||
static bool readExitCode(CalogValueT *args, int32_t argCount, CalogValueT *result, int32_t *outCode, bool *outGiven, const char *who) {
|
||||
char message[96];
|
||||
|
||||
*outCode = 0;
|
||||
*outGiven = false;
|
||||
if (argCount < 1) {
|
||||
return true;
|
||||
}
|
||||
if (args[0].type == calogIntE) {
|
||||
*outCode = (int32_t)args[0].as.i;
|
||||
} else if (args[0].type == calogRealE) {
|
||||
*outCode = (int32_t)args[0].as.r;
|
||||
} else {
|
||||
snprintf(message, sizeof(message), "%s expects an optional integer exit code", who);
|
||||
calogFail(result, calogErrArgE, message);
|
||||
return false;
|
||||
}
|
||||
*outGiven = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Ask the pump loop to stop, reporting code if nothing has named one yet (noteExitCode).
|
||||
static void requestShutdown(int32_t code) {
|
||||
noteExitCode(code);
|
||||
|
|
@ -572,7 +530,6 @@ int main(int argc, char **argv) {
|
|||
calogSetErrorHandler(calog, onError, NULL);
|
||||
|
||||
if (calogRegister(calog, "calogPrint", nativeCalogPrint, NULL) != calogOkE ||
|
||||
calogRegisterInline(calog, "calogEnd", nativeCalogEnd, NULL) != calogOkE ||
|
||||
calogRegisterInline(calog, "calogExit", nativeCalogExit, NULL) != calogOkE) {
|
||||
fprintf(stderr, "calog: failed to register the runner natives\n");
|
||||
status = 1;
|
||||
|
|
@ -639,7 +596,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
// Service script->native calls until a script calls calogExit, we are signalled, or every
|
||||
// launched context is gone -- each having either ended itself with calogEnd or errored out.
|
||||
// launched context is gone -- each having either retired itself (taskExit) or errored out.
|
||||
// Both are reaped here; when the last live context is gone, the runner exits.
|
||||
while (!atomic_load(&gShutdown)) {
|
||||
calogPump(calog);
|
||||
|
|
@ -654,9 +611,9 @@ int main(int argc, char **argv) {
|
|||
liveCount--;
|
||||
noteExitCode(1); // a script that errored out: the run did not fully succeed
|
||||
} else if (calogContextFinished(gLaunched[index].context)) {
|
||||
// calogEnd: the script said it was done, so its thread has exited. Close joins it
|
||||
// and frees the context -- the reaping the script asked for. Not a failure, so the
|
||||
// exit code is left to whatever else names one.
|
||||
// The script retired itself (taskExit) and its thread has exited. Close joins it
|
||||
// and frees the context -- the reaping that lets a self-ended script end the run when
|
||||
// it was the last one. Not a failure, so the exit code is left to whatever names one.
|
||||
calogContextClose(gLaunched[index].context);
|
||||
gLaunched[index].context = NULL;
|
||||
liveCount--;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ struct CalogContextT {
|
|||
char **allow; // sorted allowed-native names, or NULL = every native permitted
|
||||
int32_t allowCount;
|
||||
bool limited; // any limit active (adapter installs an allocator/hook; allow checked)
|
||||
_Atomic bool aborting; // calogAbortCurrent (calogEnd): this script stops, the runtime lives on
|
||||
_Atomic bool aborting; // calogAbortCurrent: this script stops, the runtime lives on
|
||||
bool closing; // guarded by broker->ctxMutex: a close already owns this context
|
||||
bool queueClosed; // guarded by queueMutex: serveLoop has stopped serving, refuse new messages
|
||||
// Every callable this context created, so it can reclaim their engine handles before its
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ static void checkImpl(bool condition, const char *message, int32_t line) {
|
|||
}
|
||||
|
||||
|
||||
// What the runner's calogEnd does: end THIS script, leaving the runtime and every other script
|
||||
// running. Inline, so it runs on the calling script's own thread and can unwind it.
|
||||
// calogAbortCurrent: end THIS script, leaving the runtime and every other script running. Inline,
|
||||
// so it runs on the calling script's own thread and can unwind it.
|
||||
static int32_t nativeEndSelf(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||
(void)args;
|
||||
(void)argCount;
|
||||
|
|
@ -212,7 +212,7 @@ static void testHeldCallableSurvivesTeardown(const char *what, const char *sourc
|
|||
|
||||
|
||||
// The other half of the rule, and the one the destroy phases cannot reach: a context that dies while
|
||||
// the RUNTIME lives on -- a script that errors out, or one that ends itself with calogEnd -- has to
|
||||
// the RUNTIME lives on -- a script that errors out, or one that ends itself -- has to
|
||||
// reclaim its handles just the same. Its interpreter is destroyed on its own thread the moment it
|
||||
// stops, long before anyone tears the runtime down.
|
||||
static void testOwnerDiesBeforeTheRuntime(const char *what, const char *source, bool expectError) {
|
||||
|
|
@ -336,7 +336,7 @@ int main(void) {
|
|||
"calogExport('e', function () {}); ready(); throw new Error('boom');", true);
|
||||
testOwnerDiesBeforeTheRuntime("a JS timer callback whose script then errors out",
|
||||
"timerEvery(1000, function () {}); ready(); throw new Error('boom');", true);
|
||||
testOwnerDiesBeforeTheRuntime("a JS subscriber whose script ends itself (calogEnd)",
|
||||
testOwnerDiesBeforeTheRuntime("a JS subscriber whose script ends itself",
|
||||
"psSubscribe('t', function () {}); ready(); endSelf();", false);
|
||||
testCrossEngineValueOutlivesOwner();
|
||||
testGuardsAfterShutdown();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue