@ai-ecoverse/slicc-kernel 1.129.0 → 1.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -388,7 +388,7 @@ The module's default export (or its `main` export) is called with a context and
388
388
  - The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
389
389
  - `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
390
390
  - `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none, `EPERM` when there were some but it may signal none of them); a process that exec'd gets the signal once. Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
391
- - `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
391
+ - `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. In the main thread it ends the process with all its threads (`Worker` itself is withheld; threads come from `thread.spawn`, below); in a thread it ends that thread only. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
392
392
  - `select({ read?, write?, timeoutMs? }, { signal }?)` waits until a descriptor of either list is readable or writable, and answers `{ read, write }` with the ready ones; `timeoutMs` (default: no limit) bounds the wait, and `0` only checks. After a nonblock write's `EAGAIN`, `select({ write: [fd] })` says when room returns ([#1124](https://github.com/ai-ecoverse/slicc-kernel/issues/1124)).
393
393
  - `socketpair()` (and `sync.socketpair()`) answers `[a, b]`, two descriptors of the program on a connected `AF_UNIX` stream pair, and `pipe()` (and `sync.pipe()`) answers `[readFd, writeFd]` ([#1093](https://github.com/ai-ecoverse/slicc-kernel/issues/1093)).
394
394
  - **The same descriptors either way:** the async and `sync` forms give the program's own kernel descriptors, which `read`, `write`, `close` and their `sync` forms all take, until the program closes them or ends.
@@ -399,6 +399,24 @@ The module's default export (or its `main` export) is called with a context and
399
399
  - **Only the named descriptors reach the child.** Others the program holds, the other end of a pair included, don't, and a grandchild gets fd 3 only if the child names it again.
400
400
  - **The parent's copy:** the program keeps `f` and closes it itself when it no longer needs it, so the child sees end of file once it's the only one left.
401
401
  - **Why a `Record`:** it can't name a child fd twice, and it reads like Node's `stdio` index, so it's this rather than a list of `{ fd, from }`.
402
+ - **`thread`, kernel threads of the same process** ([#1094](https://github.com/ai-ecoverse/slicc-kernel/issues/1094)), the base for Node's `worker_threads` (tinypool, piscina, vitest's and jest's thread pools, eslint's concurrency). `thread.spawn({ data, transfer })` resolves with `{ tid, port, exited, terminate() }`.
403
+ - **What a thread is:** a new process worker in the **same process**. It has the same pid, descriptor table, cwd, umask, credentials and lockdown, and a sync lane of its own, so `sync` calls in one thread don't hold another.
404
+ - **What it runs:** the program's same default export, called with a context whose `thread` is `{ tid, port, data }`. In the main thread `thread.tid` is `0` and `thread.port` is `undefined`. Threads are numbered from 1 per process, and a thread can spawn threads too.
405
+ - **The port** is a native `MessagePort`, one end of the platform's `MessageChannel`, which the kernel keeps from before the program runs, so a runtime that replaces the global `MessageChannel` (as a Node runtime does) still gets real ports. Messages are structured clones: a `SharedArrayBuffer` is shared (`Atomics.wait` and `Atomics.notify` work across threads), and what is listed in `transfer` (an `ArrayBuffer`, a `MessagePort`) moves and is detached in the sender. `data` is cloned the same way, with `transfer`.
406
+ - **Ending:**
407
+
408
+ | How | `exited` |
409
+ |---|---|
410
+ | `exit(n)` or `sync.exit(n)` in the thread (only that thread ends) | `n` |
411
+ | the thread's function returns `n` | `n` (nothing is `0`) |
412
+ | an uncaught error (the kernel prints nothing: a runtime reports it, as Node does with the `Worker`'s `error` event) | `1` |
413
+ | `terminate()`, which resolves once the thread has ended | `1` |
414
+ | out of memory (`RangeError: Array buffer allocation failed`, or the worker dying of it) | `thread.NO_MEMORY`, `256` |
415
+ | the worker died otherwise (nothing is printed either; the runtime reports it) | `thread.CRASHED`, `257` |
416
+
417
+ The two codes are outside 0–255, so no `exit(n)` gives them. The main thread's exit (any exit, or a signal that ends the process) ends every thread.
418
+ - **The process:** a thread ending changes nothing for `wait`, `ps` or the process's status. Signals go to the main thread only, so a thread's `signals.on`, `ignore` and `reset` change nothing (as Node, where a worker gets no signal events); the main thread's disposition is the process's. Kernel calls a thread leaves in flight when it ends (a `read` it never awaited, say) are cancelled, so they can't take another thread's data. A stopped process (`^Z`, `SIGSTOP`) stops its threads as well, timers included, until `SIGCONT`.
419
+ - **The cap:** at most twice `navigator.hardwareConcurrency` threads per process at once, and at least 16 (so piscina's default of 1.5 × the parallelism fits), or what `SLICC_JS_THREADS` in the program's environment says, up to 256. Past it, `spawn` rejects with `EAGAIN`, and an ended thread frees its slot. Each thread is a dedicated worker, like a process worker, so threads count against the browser's worker budget the same way processes do.
402
420
  - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
403
421
  - `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
404
422
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;