From 5e8e0960bc268b82e1c1cbfe3fd9f48b1a3c7fc8 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Wed, 2 Sep 2026 01:29:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(rt2):=20term.size=20+=20term.width=20?= =?UTF-8?q?=E2=80=94=20the=20wmux=20ladder's=20last=20runtime=20asks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (cherry picked from commit 1514fb46c21c4318856cfcb3ca2b4d430caba72b) --- compiler/src/types.ml | 13 ++- docs/stories/00-status.md | 1 + docs/stories/runtime-v2/06-term-size-width.md | 24 +++++ runtime/src/builtin.c | 2 +- runtime/src/loader.c | 1 + runtime/src/sysio.c | 26 ++++++ runtime/src/wob.h | 8 +- runtime/test/test_term.c | 89 +++++++++++++++++++ 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 docs/stories/runtime-v2/06-term-size-width.md diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 521b9fa..b7caf8c 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -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 diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 81a3657..0c6d43c 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -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 diff --git a/docs/stories/runtime-v2/06-term-size-width.md b/docs/stories/runtime-v2/06-term-size-width.md new file mode 100644 index 0000000..1e730a0 --- /dev/null +++ b/docs/stories/runtime-v2/06-term-size-width.md @@ -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. diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 1afaa23..ed6fbc3 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -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); diff --git a/runtime/src/loader.c b/runtime/src/loader.c index a28fc76..28b2605 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -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, diff --git a/runtime/src/sysio.c b/runtime/src/sysio.c index 0ce3cca..fd581e7 100644 --- a/runtime/src/sysio.c +++ b/runtime/src/sysio.c @@ -30,12 +30,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #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++) diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 422e662..8325c15 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -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 diff --git a/runtime/test/test_term.c b/runtime/test/test_term.c index f4a57c4..e45bdc8 100644 --- a/runtime/test/test_term.c +++ b/runtime/test/test_term.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -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"); }