feat(rt2): term.size + term.width — the wmux ladder's last runtime asks
- term.size(fd) -> ?TermSize{cols,rows}: TIOCGWINSZ, resize's read twin;
nil = not a tty (expected answer, never a trap)
- term.width(cp): libc wcwidth under C.UTF-8 (LC_CTYPE set on first
use, host-locale fallback): -1 control, 0 combining, 1, 2
- ids 108/109 (all four registrations); TermSize predeclared
- legs: PTY sized 77x33 from outside answers exactly that, pipe answers
nil, widths a/CJK/combining/BEL = 1/2/0/-1; test_term 81/0, woc 557/0
- story runtime-v2 6 recorded done; board row appended
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1514fb46c21c4318856cfcb3ca2b4d430caba72b)
This commit is contained in:
parent
6e997759e5
commit
5e8e0960bc
8 changed files with 161 additions and 3 deletions
|
|
@ -272,11 +272,18 @@ let signal_record_name = "Signal"
|
|||
|
||||
let signal_record_fields : (string * field_ty) list = [ ("sig", Scalar "Int") ]
|
||||
|
||||
(* runtime-v2 6: term.size's answer; nil = the fd is not a tty *)
|
||||
let termsize_record_name = "TermSize"
|
||||
|
||||
let termsize_record_fields : (string * field_ty) list =
|
||||
[ ("cols", Scalar "Int"); ("rows", Scalar "Int") ]
|
||||
|
||||
let predeclared_records : (string * (string * field_ty) list) list =
|
||||
[ (error_record_name, error_record_fields); (stat_record_name, stat_record_fields);
|
||||
(time_record_name, time_record_fields); (proc_record_name, proc_record_fields);
|
||||
(child_record_name, child_record_fields);
|
||||
(signal_record_name, signal_record_fields) ]
|
||||
(signal_record_name, signal_record_fields);
|
||||
(termsize_record_name, termsize_record_fields) ]
|
||||
|
||||
(* One member of a reserved stdlib module (`fs.stat`, `net.write`, ...).
|
||||
[sm_builtin] is its .wob builtin id (runtime/src/wob.h); [sm_record] names
|
||||
|
|
@ -357,6 +364,10 @@ let stdlib_members : stdlib_member list =
|
|||
m "net" "send_fd" 2 105 (Some (TScalar "Bool")) None;
|
||||
m "net" "recv_fd" 1 106 (Some (TNullable (TScalar "Int"))) None;
|
||||
m "net" "connect_unix" 1 107 (Some (TScalar "Int")) None;
|
||||
(* runtime-v2 6: resize's read twin (nil = not a tty), and a
|
||||
codepoint's terminal cell width (libc wcwidth under C.UTF-8) *)
|
||||
m "term" "size" 1 108 (Some (TNullable (TScalar termsize_record_name))) (Some termsize_record_name);
|
||||
m "term" "width" 1 109 (Some (TScalar "Int")) None;
|
||||
(* json — both members are lowered specially (emit.ml): encode needs its
|
||||
argument's static kind, and decode has no type until an `as` names one,
|
||||
so neither goes through the generic builtin path. They are listed here
|
||||
|
|
|
|||
|
|
@ -1247,6 +1247,7 @@ starts. Edges in [dependency graph section 6](../00-dependency-graph.md).
|
|||
| 3 | [signals as events](runtime-v2/03-signals-as-events.md) | ✅ **DONE 2026-09-02** — `signal.on(sig, addr)` delivering a fresh Signal record (scalar payloads crash by construction — spec amendment); handler-latch + wake eventfd instead of signalfd (amendment); TERM/INT refused by name |
|
||||
| 4 | [termios adoption](runtime-v2/04-termios.md) | ✅ **DONE 2026-09-02** — `term.raw/restore`; restore proven a runtime obligation twice (DIV0 while raw, and the double-raw refusal itself) |
|
||||
| 5 | [fd passing](runtime-v2/05-fd-passing.md) | ✅ **DONE 2026-09-02** — `net.send_fd`/`recv_fd`/`connect_unix`; a tty crossed the socket, was raw'd through the received copy and restored at destroy — the wmux handover in miniature |
|
||||
| 6 | [term.size + term.width](runtime-v2/06-term-size-width.md) | ✅ **DONE 2026-09-02** — TIOCGWINSZ read twin (nil = not a tty) and libc wcwidth under C.UTF-8; the only runtime work the whole wmux parity ladder needs |
|
||||
|
||||
### ▸ wmux — the terminal multiplexer track
|
||||
|
||||
|
|
|
|||
24
docs/stories/runtime-v2/06-term-size-width.md
Normal file
24
docs/stories/runtime-v2/06-term-size-width.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
track: runtime-v2
|
||||
iteration: "6"
|
||||
status: done
|
||||
readiness: ready
|
||||
---
|
||||
|
||||
# runtime-v2 6 — term.size and term.width: the two verbs the wmux ladder asked for
|
||||
|
||||
> Part of [Story — runtime-v2: the runtime beyond sockets](00-story.md).
|
||||
> Named by the wmux parity-ladder brainstorm (2026-09-02) as the ONLY
|
||||
> runtime work all nine wmux rungs need beyond the landed track.
|
||||
>
|
||||
> **DONE 2026-09-02, same day.** `term.size(fd) -> ?TermSize {cols,
|
||||
> rows}` — TIOCGWINSZ, the resize verb's read twin; nil = not a tty (an
|
||||
> expected answer, never a trap). `term.width(cp) -> Int` — libc
|
||||
> `wcwidth` under `C.UTF-8` (first use sets `LC_CTYPE`; falls back to
|
||||
> the host locale): -1 control, 0 combining, 1, 2. Ids 108/109;
|
||||
> `TermSize` joins the predeclared records. Consumers: wmux 1 (attach
|
||||
> at the client's real size), wmux 2 (grid column math), wmux 6
|
||||
> (multi-client min-size, SIGWINCH forwarding).
|
||||
>
|
||||
> Legs in `test_term.c`: a PTY sized 77×33 from outside answers exactly
|
||||
> that; a pipe answers nil; widths a=1, U+4E2D=2, U+0301=0, BEL=-1.
|
||||
|
|
@ -172,7 +172,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE)
|
||||
return wo_builtin_json(vm, R, ins, msg);
|
||||
if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS
|
||||
|| (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT_UNIX))
|
||||
|| (C >= WO_B_NET_READ_DL && C <= WO_B_TERM_WIDTH))
|
||||
return wo_builtin_sys(vm, R, ins, msg);
|
||||
if (C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256)
|
||||
return wo_builtin_crypto(vm, R, ins, msg);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
|
|||
[WO_B_PROC_SPAWN_PTY] = 5, [WO_B_PROC_RESIZE] = 3,
|
||||
[WO_B_SIGNAL_ON] = 3, [WO_B_TERM_RAW] = 1, [WO_B_TERM_RESTORE] = 1,
|
||||
[WO_B_NET_SEND_FD] = 2, [WO_B_NET_RECV_FD] = 1, [WO_B_NET_CONNECT_UNIX] = 1,
|
||||
[WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1,
|
||||
/* json (json.c): encode takes the value's static kind, decode the class
|
||||
id to build */
|
||||
[WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2, [WO_B_MAP_GET_OPT] = 2,
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <locale.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include "builtin.h"
|
||||
#include "cont.h"
|
||||
|
|
@ -1643,6 +1645,30 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
R[A] = WO_NIL_SCALAR; /* plain bytes (or EOF): no fd arrived */
|
||||
return 0;
|
||||
}
|
||||
/* ---- runtime-v2 6: the size read-twin and cell width -------------- */
|
||||
case WO_B_TERM_SIZE: { /* (fd, cls) -> ?TermSize {cols, rows} */
|
||||
struct winsize ws;
|
||||
if (ioctl((int)(int64_t)R[B], TIOCGWINSZ, &ws) != 0) {
|
||||
R[A] = 0; /* not a tty: nil, an EXPECTED answer */
|
||||
return 0;
|
||||
}
|
||||
wo_hdr *o = record_of(vm, R[B + 1], 2, msg);
|
||||
if (!o) return R[B + 1] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
|
||||
uint64_t *fp = wo_fields(o);
|
||||
fp[0] = (uint64_t)ws.ws_col;
|
||||
fp[1] = (uint64_t)ws.ws_row;
|
||||
R[A] = (uint64_t)(uintptr_t)o;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_TERM_WIDTH: { /* (codepoint) -> cell width via wcwidth */
|
||||
static int loc_inited = 0;
|
||||
if (!loc_inited) {
|
||||
loc_inited = 1;
|
||||
if (!setlocale(LC_CTYPE, "C.UTF-8")) setlocale(LC_CTYPE, "");
|
||||
}
|
||||
R[A] = (uint64_t)(int64_t)wcwidth((wchar_t)(int64_t)R[B]);
|
||||
return 0;
|
||||
}
|
||||
case WO_B_TERM_RESTORE: { /* (fd) -> 0 from the saved entry */
|
||||
int fd = (int)(int64_t)R[B];
|
||||
for (int i = 0; i < 8; i++)
|
||||
|
|
|
|||
|
|
@ -541,9 +541,15 @@ enum {
|
|||
* the peer sent plain bytes */
|
||||
WO_B_NET_CONNECT_UNIX = 107, /* (path) -> Int: AF_UNIX client fd,
|
||||
* nonblocking */
|
||||
/* ---- runtime-v2 6: the size read-twin and cell width (wmux 2/6) */
|
||||
WO_B_TERM_SIZE = 108, /* (fd, cls) -> ?TermSize {cols, rows}:
|
||||
* TIOCGWINSZ; nil = not a tty */
|
||||
WO_B_TERM_WIDTH = 109, /* (codepoint) -> Int cell width via libc
|
||||
* wcwidth under C.UTF-8: -1 control,
|
||||
* 0 combining, 1, or 2 */
|
||||
};
|
||||
|
||||
#define WO_B_MAX 107u
|
||||
#define WO_B_MAX 109u
|
||||
/* ids at or above this one live in sysio.c, not builtin.c */
|
||||
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
|
@ -404,9 +405,97 @@ static void test_fd_passing(void) {
|
|||
unlink("/tmp/wo-rt2-fdpass.sock");
|
||||
}
|
||||
|
||||
/* ---- runtime-v2 6: term.size + term.width -------------------------------
|
||||
* methods: 0 size(fd) -> ?TermSize, 1 width(cp) -> Int */
|
||||
static uint8_t *size_module(size_t *len) {
|
||||
wb_t *b = wb_new();
|
||||
uint32_t kts = wb_const_text(b, "TermSize");
|
||||
uint32_t ksz = wb_const_text(b, "size");
|
||||
uint32_t kw = wb_const_text(b, "width");
|
||||
uint8_t kinds[2] = {WO_K_SCALAR, WO_K_SCALAR};
|
||||
uint32_t cls = wb_class(b, kts, 0, kinds, 2);
|
||||
uint32_t kcls = wb_const_int(b, (int64_t)cls);
|
||||
{ /* size(fd) */
|
||||
uint32_t code[6];
|
||||
uint32_t n = 0;
|
||||
code[n++] = wo_ins_abc(WOP_MOVE, 1, 0, 0);
|
||||
code[n++] = wo_ins_abx(WOP_LOADK, 2, (uint16_t)kcls);
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 3, 1, WO_B_TERM_SIZE);
|
||||
code[n++] = wo_ins_abc(WOP_RET, 3, 0, 0);
|
||||
wb_method(b, ksz, WOB_NONE, 1, 4, code, n, NULL, 0, NULL, 0);
|
||||
}
|
||||
{ /* width(cp) */
|
||||
uint32_t code[4];
|
||||
uint32_t n = 0;
|
||||
code[n++] = wo_ins_abc(WOP_BUILTIN, 1, 0, WO_B_TERM_WIDTH);
|
||||
code[n++] = wo_ins_abc(WOP_RET, 1, 0, 0);
|
||||
wb_method(b, kw, WOB_NONE, 1, 2, code, n, NULL, 0, NULL, 0);
|
||||
}
|
||||
return wb_finish(b, len);
|
||||
}
|
||||
|
||||
static void test_size_and_width(void) {
|
||||
size_t len;
|
||||
uint8_t *img = size_module(&len);
|
||||
wo_module mod;
|
||||
char lerr[256];
|
||||
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
|
||||
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
|
||||
uint64_t ret = 0;
|
||||
wo_err err;
|
||||
|
||||
/* a PTY sized 77x33 from the outside answers exactly that */
|
||||
int master = posix_openpt(O_RDWR | O_NOCTTY);
|
||||
T_CHECK(master >= 0);
|
||||
T_EQ(grantpt(master), 0);
|
||||
T_EQ(unlockpt(master), 0);
|
||||
struct winsize ws = {.ws_row = 33, .ws_col = 77};
|
||||
T_EQ(ioctl(master, TIOCSWINSZ, &ws), 0);
|
||||
uint64_t sargs[1] = {(uint64_t)master};
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, sargs, 1, &ret, &err), 0);
|
||||
T_CHECK(ret != 0 && ret != WO_NIL_SCALAR);
|
||||
if (ret != 0 && ret != WO_NIL_SCALAR) {
|
||||
wo_hdr *o = (wo_hdr *)(uintptr_t)ret;
|
||||
T_EQ(wo_fields(o)[0], 77u);
|
||||
T_EQ(wo_fields(o)[1], 33u);
|
||||
wo_drop_obj(&VM.rt, o);
|
||||
}
|
||||
close(master);
|
||||
|
||||
/* a pipe is not a tty: nil */
|
||||
int p[2];
|
||||
T_EQ(pipe(p), 0);
|
||||
uint64_t pargs[1] = {(uint64_t)p[0]};
|
||||
memset(&err, 0, sizeof err);
|
||||
T_EQ(wo_vm_call(&VM, 0, pargs, 1, &ret, &err), 0);
|
||||
T_EQ(ret, 0u); /* ?TermSize nil is the zero word (heap-shaped) */
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
|
||||
/* widths under C.UTF-8: ascii 1, CJK 2, combining 0, control -1 */
|
||||
uint64_t wa[1] = {97};
|
||||
T_EQ(wo_vm_call(&VM, 1, wa, 1, &ret, &err), 0);
|
||||
T_EQ((int64_t)ret, 1);
|
||||
uint64_t wc[1] = {0x4E2D};
|
||||
T_EQ(wo_vm_call(&VM, 1, wc, 1, &ret, &err), 0);
|
||||
T_EQ((int64_t)ret, 2);
|
||||
uint64_t wm[1] = {0x0301};
|
||||
T_EQ(wo_vm_call(&VM, 1, wm, 1, &ret, &err), 0);
|
||||
T_EQ((int64_t)ret, 0);
|
||||
uint64_t wb_[1] = {7};
|
||||
T_EQ(wo_vm_call(&VM, 1, wb_, 1, &ret, &err), 0);
|
||||
T_EQ((int64_t)ret, -1);
|
||||
|
||||
wo_vm_destroy(&VM);
|
||||
wo_module_free(&mod);
|
||||
free(img);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_signal_on_delivers_record();
|
||||
test_term_raw_restore();
|
||||
test_fd_passing();
|
||||
test_size_and_width();
|
||||
return t_report("test_term");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue