diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70227377..9d649048 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,15 +466,16 @@ jobs: grep -q "needs formatting" /tmp/fmt-check.err "$SCUZZ" build examples/kernel "$SCUZZ" build examples/kernel 2>&1 | tee /tmp/incr.out - grep -q "up-to-date" /tmp/incr.out + if grep -q "^ok$" /tmp/incr.out; then echo "fingerprint hit should not rebuild" && exit 1; fi + test -x examples/kernel/build/kernel # Path-dep invalidation: a change in a dependency source must rebuild the root. "$SCUZZ" build --full examples/counter "$SCUZZ" build examples/counter 2>&1 | tee /tmp/counter-incr.out - grep -q "up-to-date" /tmp/counter-incr.out + if grep -q "^ok$" /tmp/counter-incr.out; then echo "fingerprint hit should not rebuild" && exit 1; fi cp examples/shared/src/Shared.scuzz /tmp/shared-orig.scuzz printf 'def counterTitle(): String =\n "Counter"\n\ndef countLabel(n: Int): String =\n s"count = $n!"\n' > examples/shared/src/Shared.scuzz "$SCUZZ" build examples/counter 2>&1 | tee /tmp/counter-inval.out - if grep -q "up-to-date" /tmp/counter-inval.out; then echo "dependency edit should invalidate fingerprint" && exit 1; fi + if ! grep -q "^ok$" /tmp/counter-inval.out; then echo "dependency edit should invalidate fingerprint" && exit 1; fi cp /tmp/shared-orig.scuzz examples/shared/src/Shared.scuzz "$SCUZZ" build --full examples/counter diff --git a/crates/ffi-skia/Makefile b/crates/ffi-skia/Makefile index eca6d974..61210871 100644 --- a/crates/ffi-skia/Makefile +++ b/crates/ffi-skia/Makefile @@ -1,12 +1,17 @@ CC ?= clang +CXX ?= clang++ CFLAGS ?= -std=c11 -Wall -Wextra -Werror -O2 ROOT := $(abspath ../..) TRIPLE ?= $(shell $(ROOT)/scripts/skia_triple.sh) PREBUILT_DIR := $(ROOT)/third_party/skia/prebuilt/$(TRIPLE) PREBUILT_LIB := $(PREBUILT_DIR)/libsk_capi.a +PIN := $(ROOT)/third_party/skia/PIN +SKIA_BRANCH := $(shell awk -F= '/^skia_branch=/{print substr($$0,13); exit}' "$(PIN)") +SKIA_HDR_URL := https://skia.googlesource.com/skia/+archive/$(SKIA_BRANCH)/include.tar.gz INCLUDES := -Iinclude -Isrc SRC := src/sk_sw.c src/png_enc.c src/sk_mono.c OBJ := $(SRC:src/%.c=build/%.o) +SHIM_OBJS := build/sk_capi_skia.o build/sk_capi_skia_bridge.o .PHONY: all clean test lib lib-skia lib-sk-sw lib-gpu ensure-prebuilt @@ -39,8 +44,10 @@ ensure-prebuilt: UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GPU_LIBS := -framework OpenGL +SKIA_LIBS := -lc++ -lm -lz -lbz2 -framework CoreFoundation -framework CoreGraphics -framework CoreText -framework Foundation -framework Carbon else GPU_LIBS := -lEGL -lGLESv2 +SKIA_LIBS := -lstdc++ -lm -lz -lbz2 endif build/%.o: src/%.c include/sk_capi.h src/png_enc.h src/sk_gpu.h | build @@ -49,6 +56,24 @@ build/%.o: src/%.c include/sk_capi.h src/png_enc.h src/sk_gpu.h | build build/sk_gpu.o: src/sk_gpu.c src/sk_gpu.h | build $(CC) $(CFLAGS) -Wno-deprecated-declarations $(INCLUDES) -c $< -o $@ +# Headers that match the pin's Skia branch. Not a vendored tree. +build/skia-src/.stamp: | build + @mkdir -p build/skia-src/include + @echo "ffi-skia: fetch $(SKIA_BRANCH) include for shim relink" + @curl -fsSL "$(SKIA_HDR_URL)" -o build/skia-include.tar.gz + @rm -rf build/skia-src/include + @mkdir -p build/skia-src/include + @tar -xzf build/skia-include.tar.gz -C build/skia-src/include + @test -f build/skia-src/include/core/SkCanvas.h + @echo "$(SKIA_BRANCH)" > $@ + +build/sk_capi_skia.o: src/sk_capi_skia.cpp include/sk_capi.h build/skia-src/.stamp + $(CXX) -std=c++17 -O2 -fPIC -c src/sk_capi_skia.cpp -o $@ \ + -Ibuild/skia-src -DSK_RELEASE -DSCUZZ_SKIA_EMBEDDED_FONT + +build/sk_capi_skia_bridge.o: src/sk_capi_skia_bridge.c include/sk_capi.h | build + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + lib-sk-sw: $(OBJ) build/sk_gpu_none.o | build @if [ -f build/libsk_capi.a ] && [ -f build/sk_capi_backend ] && grep -qx sk_sw build/sk_capi_backend; then \ fresh=1; \ @@ -73,35 +98,58 @@ lib-gpu: $(OBJ) build/sk_gpu.o | build ar rcs build/libsk_capi.a $(OBJ) build/sk_gpu.o; \ echo gpu > build/sk_capi_backend -lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o | build +# Pin supplies Skia objects + font. Replace the shim so the C ABI matches +# sk_capi.h (save/clip/restore, RGBA peek). Fail if clip symbols are missing. +lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o $(SHIM_OBJS) | build @test -f "$(PREBUILT_LIB)" @if [ -f build/libsk_capi.a ] && [ -f build/sk_capi_backend ] && grep -qx skia build/sk_capi_backend \ && [ ! "$(PREBUILT_LIB)" -nt build/libsk_capi.a ] \ && [ ! build/sk_gpu_skia_stub.o -nt build/libsk_capi.a ] \ - && [ ! build/sk_mono.o -nt build/libsk_capi.a ]; then \ - exit 0; \ + && [ ! build/sk_mono.o -nt build/libsk_capi.a ] \ + && [ ! build/sk_capi_skia.o -nt build/libsk_capi.a ] \ + && [ ! build/sk_capi_skia_bridge.o -nt build/libsk_capi.a ]; then \ + if nm build/libsk_capi.a | grep -E ' T _?sk_canvas_clip_rect' >/dev/null && \ + nm build/libsk_capi.a | grep -E ' T _?sk_canvas_save' >/dev/null && \ + nm build/libsk_capi.a | grep -E ' T _?sk_canvas_restore' >/dev/null; then \ + exit 0; \ + fi; \ fi; \ rm -f build/*.a; \ cp -f "$(PREBUILT_LIB)" build/libsk_capi.a; \ - ar rcs build/libsk_capi.a build/sk_gpu_skia_stub.o build/sk_mono.o; \ + ar d build/libsk_capi.a sk_capi_skia.o sk_capi_skia_bridge.o; \ + ar rcs build/libsk_capi.a $(SHIM_OBJS) build/sk_gpu_skia_stub.o build/sk_mono.o; \ + if ! nm build/libsk_capi.a | grep -E ' T _?sk_canvas_clip_rect' >/dev/null; then \ + echo "ffi-skia: linked archive lacks sk_canvas_clip_rect" >&2; \ + exit 1; \ + fi; \ + if ! nm build/libsk_capi.a | grep -E ' T _?sk_canvas_save' >/dev/null; then \ + echo "ffi-skia: linked archive lacks sk_canvas_save" >&2; \ + exit 1; \ + fi; \ + if ! nm build/libsk_capi.a | grep -E ' T _?sk_canvas_restore' >/dev/null; then \ + echo "ffi-skia: linked archive lacks sk_canvas_restore" >&2; \ + exit 1; \ + fi; \ echo skia > build/sk_capi_backend build/test_skia: tests/test_skia.c lib @extra=""; \ if [ -f build/sk_capi_backend ] && grep -qx skia build/sk_capi_backend; then \ - if [ "$$(uname -s)" = Darwin ]; then \ - extra="-lc++ -lm -lz -lbz2 -framework CoreFoundation -framework CoreGraphics -framework CoreText -framework Foundation -framework Carbon"; \ - else \ - extra="-lstdc++ -lm -lz -lbz2"; \ - fi; \ + extra="$(SKIA_LIBS)"; \ elif [ -f build/sk_capi_backend ] && grep -qx gpu build/sk_capi_backend; then \ extra="$(GPU_LIBS)"; \ fi; \ $(CC) $(CFLAGS) $(INCLUDES) tests/test_skia.c -Lbuild -lsk_capi $$extra -lpthread -o $@ -# Canvas clip against in-tree sk_sw. The pinned Skia archive does not export save/clip. -build/test_sk_clip: tests/test_sk_clip.c src/sk_sw.c src/png_enc.c src/sk_gpu_none.c include/sk_capi.h src/png_enc.h | build - $(CC) $(CFLAGS) $(INCLUDES) tests/test_sk_clip.c src/sk_sw.c src/png_enc.c src/sk_gpu_none.c -o $@ +# Clip / RGBA / UTF-8 against the archive `lib` actually built. +build/test_sk_clip: tests/test_sk_clip.c lib + @extra=""; \ + if [ -f build/sk_capi_backend ] && grep -qx skia build/sk_capi_backend; then \ + extra="$(SKIA_LIBS)"; \ + elif [ -f build/sk_capi_backend ] && grep -qx gpu build/sk_capi_backend; then \ + extra="$(GPU_LIBS)"; \ + fi; \ + $(CC) $(CFLAGS) $(INCLUDES) tests/test_sk_clip.c -Lbuild -lsk_capi $$extra -lpthread -o $@ build/test_sk_gpu: tests/test_sk_gpu.c lib $(CC) $(CFLAGS) $(INCLUDES) tests/test_sk_gpu.c -Lbuild -lsk_capi $(GPU_LIBS) -lpthread -o $@ diff --git a/crates/ffi-skia/README.md b/crates/ffi-skia/README.md index c660775e..377a7f71 100644 --- a/crates/ffi-skia/README.md +++ b/crates/ffi-skia/README.md @@ -3,7 +3,8 @@ Thin Skia-shaped C ABI (`include/sk_capi.h`) for Headless/Desktop paint. **Default:** pinned Skia CPU prebuilt through `scripts/fetch_skia.sh` / -`third_party/skia/PIN` → `build/sk_capi_backend` = `skia`. +`third_party/skia/PIN` → `build/sk_capi_backend` = `skia`. `make lib` compiles +the in-tree shim into that archive so the C ABI matches `sk_capi.h`. **Opt out:** `SCUZZ_SKIA=sk_sw` builds in-tree `src/sk_sw.c`. **GPU presenter:** `SCUZZ_SKIA=gpu` paints with `sk_sw` and presents through OpenGL (upload + readback). Missing OpenGL fails with one install line. diff --git a/crates/ffi-skia/src/png_enc.c b/crates/ffi-skia/src/png_enc.c index dcd88002..a726da06 100644 --- a/crates/ffi-skia/src/png_enc.c +++ b/crates/ffi-skia/src/png_enc.c @@ -1,5 +1,6 @@ #include "png_enc.h" +#include #include #include @@ -159,8 +160,10 @@ uint8_t *sz_png_encode_rgba(const uint8_t *pixels, int width, int height, return NULL; if (stride <= 0) stride = width * 4; + if ((size_t)width > (SIZE_MAX - 1u) / 4u) + return NULL; - filt_len = (size_t)(width * 4 + 1) * (size_t)height; + filt_len = ((size_t)width * 4u + 1u) * (size_t)height; filt = (uint8_t *)malloc(filt_len); if (!filt) return NULL; diff --git a/crates/ffi-skia/src/sk_capi_skia.cpp b/crates/ffi-skia/src/sk_capi_skia.cpp index aacc85fe..9076758d 100644 --- a/crates/ffi-skia/src/sk_capi_skia.cpp +++ b/crates/ffi-skia/src/sk_capi_skia.cpp @@ -68,8 +68,8 @@ extern "C" { void *scuzz_skia_surface_make(int width, int height) { if (width <= 0 || height <= 0) return nullptr; - sk_sp surf = - SkSurfaces::Raster(SkImageInfo::MakeN32Premul(width, height)); + sk_sp surf = SkSurfaces::Raster(SkImageInfo::Make( + width, height, kRGBA_8888_SkColorType, kPremul_SkAlphaType)); if (!surf) return nullptr; auto *out = new CapSurface(); @@ -246,6 +246,8 @@ int scuzz_skia_encode_png(const void *surface, uint8_t **out_bytes, int scuzz_skia_encode_png_to_file(const void *surface, const char *path) { uint8_t *bytes = nullptr; size_t len = 0; + if (!path) + return 0; if (!scuzz_skia_encode_png(surface, &bytes, &len)) return 0; SkFILEWStream stream(path); diff --git a/crates/ffi-skia/src/sk_gpu.c b/crates/ffi-skia/src/sk_gpu.c index c703a780..1211f1b7 100644 --- a/crates/ffi-skia/src/sk_gpu.c +++ b/crates/ffi-skia/src/sk_gpu.c @@ -106,16 +106,36 @@ static int gpu_make_current(void) { } if (!gpu_init_display()) return 0; - if (!eglChooseConfig(g_dpy, cfg_attr, &cfg, 1, &n) || n < 1) + if (!eglChooseConfig(g_dpy, cfg_attr, &cfg, 1, &n) || n < 1) { + eglTerminate(g_dpy); + g_dpy = EGL_NO_DISPLAY; return 0; + } g_surf = eglCreatePbufferSurface(g_dpy, cfg, pb_attr); - if (g_surf == EGL_NO_SURFACE) + if (g_surf == EGL_NO_SURFACE) { + eglTerminate(g_dpy); + g_dpy = EGL_NO_DISPLAY; return 0; + } eglBindAPI(EGL_OPENGL_ES_API); g_ctx = eglCreateContext(g_dpy, cfg, EGL_NO_CONTEXT, ctx_attr); - if (g_ctx == EGL_NO_CONTEXT) + if (g_ctx == EGL_NO_CONTEXT) { + eglDestroySurface(g_dpy, g_surf); + g_surf = EGL_NO_SURFACE; + eglTerminate(g_dpy); + g_dpy = EGL_NO_DISPLAY; + return 0; + } + if (eglMakeCurrent(g_dpy, g_surf, g_surf, g_ctx) != EGL_TRUE) { + eglDestroyContext(g_dpy, g_ctx); + eglDestroySurface(g_dpy, g_surf); + g_ctx = EGL_NO_CONTEXT; + g_surf = EGL_NO_SURFACE; + eglTerminate(g_dpy); + g_dpy = EGL_NO_DISPLAY; return 0; - return eglMakeCurrent(g_dpy, g_surf, g_surf, g_ctx) == EGL_TRUE; + } + return 1; } #endif diff --git a/crates/ffi-skia/src/sk_sw.c b/crates/ffi-skia/src/sk_sw.c index 66325142..97118faa 100644 --- a/crates/ffi-skia/src/sk_sw.c +++ b/crates/ffi-skia/src/sk_sw.c @@ -187,8 +187,12 @@ void sk_canvas_draw_rect(SkCanvas *canvas, float x, float y, float w, float h, } void sk_canvas_save(SkCanvas *canvas) { - if (!canvas || canvas->save_n >= SK_CLIP_STACK) + if (!canvas) return; + if (canvas->save_n >= SK_CLIP_STACK) { + fputs("sk_capi: clip save stack is full\n", stderr); + abort(); + } canvas->save_x0[canvas->save_n] = canvas->clip_x0; canvas->save_y0[canvas->save_n] = canvas->clip_y0; canvas->save_x1[canvas->save_n] = canvas->clip_x1; @@ -328,12 +332,36 @@ static const uint8_t FONT8[95][8] = { {0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, }; +/* Same walk as sk_mono.c: one Unicode code point is one cell. */ +static int sw_utf8_clen(const char *s) { + unsigned char c; + if (!s || !s[0]) + return 0; + c = (unsigned char)s[0]; + if (c < 0x80) + return 1; + if ((c & 0xe0) == 0xc0) + return s[1] ? 2 : 1; + if ((c & 0xf0) == 0xe0) + return (s[1] && s[2]) ? 3 : 1; + if ((c & 0xf8) == 0xf0) + return (s[1] && s[2] && s[3]) ? 4 : 1; + return 1; +} + float sk_font_measure_string(const char *text, float font_px) { - size_t n; + const char *p; + int n = 0; float px = font_px > 0.f ? font_px : 8.f; if (!text) return 0.f; - n = strlen(text); + for (p = text; *p;) { + int clen = sw_utf8_clen(p); + if (clen < 1) + clen = 1; + p += clen; + n++; + } return (float)n * px; } @@ -353,10 +381,13 @@ void sk_canvas_draw_string(SkCanvas *canvas, const char *text, float x, float y, if (baseline < 0) baseline = 0; cx = (int)x; - for (p = text; *p; p++) { + for (p = text; *p;) { + int clen = sw_utf8_clen(p); unsigned char ch = (unsigned char)*p; const uint8_t *glyph; int gy = (int)y - baseline; + if (clen < 1) + clen = 1; if (ch < 32 || ch > 126) ch = '?'; glyph = FONT8[ch - 32]; @@ -384,6 +415,7 @@ void sk_canvas_draw_string(SkCanvas *canvas, const char *text, float x, float y, } } cx += advance; + p += clen; } } @@ -448,7 +480,7 @@ int sk_encode_png_to_file(const SkSurface *surface, const char *path) { size_t len = 0; FILE *f; size_t n; - if (!sk_encode_png(surface, &bytes, &len)) + if (!path || !sk_encode_png(surface, &bytes, &len)) return 0; f = fopen(path, "wb"); if (!f) { diff --git a/crates/ffi-skia/tests/test_sk_clip.c b/crates/ffi-skia/tests/test_sk_clip.c index efca1fba..b387ab25 100644 --- a/crates/ffi-skia/tests/test_sk_clip.c +++ b/crates/ffi-skia/tests/test_sk_clip.c @@ -1,5 +1,4 @@ -/* Canvas clip against in-tree sk_sw. The pinned Skia archive does not - * export save/clip/restore. */ +/* Canvas clip against the archive `make lib` actually built. */ #include "sk_capi.h" #include @@ -25,7 +24,7 @@ int main(void) { sk_canvas_restore(canvas); px = sk_surface_peek_pixels(surf, &px_len); assert(px && px_len == 32 * 32 * 4); - /* Inside clip: fill. Outside: clear color. */ + /* Inside clip: fill. Outside: clear color. Peek is RGBA. */ assert(px[(10 * 32 + 10) * 4] == 240); assert(px[(10 * 32 + 10) * 4 + 1] == 240); assert(px[(10 * 32 + 10) * 4 + 2] == 240); diff --git a/crates/ffi-skia/tests/test_skia.c b/crates/ffi-skia/tests/test_skia.c index 3c8c5d75..7b30b7d5 100644 --- a/crates/ffi-skia/tests/test_skia.c +++ b/crates/ffi-skia/tests/test_skia.c @@ -5,6 +5,18 @@ #include #include +static int cell_has_ink(const uint8_t *px, int w, int x0, int x1, int y0, int y1) { + int x, y; + for (y = y0; y < y1; y++) { + for (x = x0; x < x1; x++) { + const uint8_t *p = px + ((size_t)y * (size_t)w + (size_t)x) * 4; + if (p[0] > 40 || p[1] > 40 || p[2] > 40) + return 1; + } + } + return 0; +} + int main(void) { SkSurface *surf = sk_surface_make_raster_n32_premul(64, 32); SkCanvas *canvas; @@ -61,6 +73,84 @@ int main(void) { assert(saw_light); } + /* Peek is row-major RGBA. Red is byte 0 with no channel remap. */ + { + SkSurface *reds = sk_surface_make_raster_n32_premul(4, 4); + SkCanvas *rc; + const uint8_t *rp; + size_t rn = 0; + assert(reds); + rc = sk_surface_get_canvas(reds); + sk_paint_set_color(paint, sk_color_rgba(255, 0, 0, 255)); + sk_canvas_draw_rect(rc, 0, 0, 4, 4, paint); + rp = sk_surface_peek_pixels(reds, &rn); + assert(rp && rn >= 4); + assert(rp[0] == 255 && rp[1] < 50 && rp[2] < 50); + sk_surface_unref(reds); + } + + /* UTF-8: one code point is one cell on sk_sw. Never two cells for "é". */ + { + float e = sk_font_measure_string("e", 8.f); + float acute = sk_font_measure_string("é", 8.f); + float ee = sk_font_measure_string("ee", 8.f); + assert(e > 0.f); + assert(acute > 0.f); + assert(acute != ee); + if (sk_font_measure_string("i", 8.f) == sk_font_measure_string("W", 8.f)) + assert(acute == e); + } + + /* draw_mono("éx"): x stays in cell 1. Cell 2 stays empty. */ + { + SkSurface *mono = sk_surface_make_raster_n32_premul(48, 16); + SkCanvas *mc; + const uint8_t *mp; + size_t mn = 0; + int cw; + assert(mono); + mc = sk_surface_get_canvas(mono); + sk_canvas_clear(mc, sk_color_rgba(0, 0, 0, 255)); + sk_paint_set_color(paint, sk_color_rgba(240, 240, 240, 255)); + sk_paint_set_text_size(paint, 8.f); + sk_canvas_draw_mono_string(mc, "éx", 0, 8, paint); + mp = sk_surface_peek_pixels(mono, &mn); + assert(mp && mn == 48 * 16 * 4); + cw = (int)(sk_font_mono_cell(8.f) + 0.5f); + if (cw < 1) + cw = 1; + assert(cell_has_ink(mp, 48, 0, cw, 0, 16)); + assert(cell_has_ink(mp, 48, cw, cw * 2, 0, 16)); + assert(!cell_has_ink(mp, 48, cw * 2, cw * 3, 0, 16)); + sk_surface_unref(mono); + } + + /* save / clip / restore on the archive this test linked. */ + { + SkSurface *clip = sk_surface_make_raster_n32_premul(32, 32); + SkCanvas *cc; + const uint8_t *cp; + size_t cn = 0; + assert(clip); + cc = sk_surface_get_canvas(clip); + sk_canvas_clear(cc, sk_color_rgba(20, 40, 80, 255)); + sk_canvas_save(cc); + sk_canvas_clip_rect(cc, 8, 8, 12, 12); + sk_paint_set_color(paint, sk_color_rgba(240, 240, 240, 255)); + sk_canvas_draw_rect(cc, 0, 0, 32, 32, paint); + sk_canvas_restore(cc); + cp = sk_surface_peek_pixels(clip, &cn); + assert(cp && cn == 32 * 32 * 4); + assert(cp[(10 * 32 + 10) * 4] == 240); + assert(cp[(10 * 32 + 10) * 4 + 1] == 240); + assert(cp[(10 * 32 + 10) * 4 + 2] == 240); + assert(cp[0] == 20); + assert(cp[1] == 40); + assert(cp[2] == 80); + assert(cp[(20 * 32 + 20) * 4] == 20); + sk_surface_unref(clip); + } + assert(sk_encode_png(surf, &png, &png_len)); assert(png_len > 8); assert(png[0] == 137 && png[1] == 80 && png[2] == 78 && png[3] == 71); diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index de353abd..265e617b 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -60,6 +60,8 @@ enum { void *sz_rc_alloc(size_t size, uint32_t kind); void sz_retain(void *ptr); void sz_release(void *ptr); +/* RC kind of `ptr`. A non-RC pointer is `SZ_RC_KIND_COUNT`. */ +uint32_t sz_rc_kind(const void *ptr); /* Live heap through sz_alloc/sz_free (user bytes; excludes size header). */ void sz_alloc_stats(size_t *live_bytes, size_t *live_count); /* Sum of RC counts on live RC blocks. Raw sz_alloc blocks add 0. */ @@ -118,6 +120,7 @@ SzString *sz_string_slice(const SzString *s, int64_t start, int64_t end); int sz_string_eq(const SzString *a, const SzString *b); int64_t sz_string_char_at(const SzString *s, int64_t index); /* byte as i64; -1 OOB */ SzString *sz_string_from_int(int64_t n); +SzString *sz_string_from_bool(int64_t b); SzString *sz_string_from_float(double x); int64_t sz_string_index_of(const SzString *s, const SzString *needle); int64_t sz_string_last_index_of(const SzString *s, const SzString *needle); @@ -165,9 +168,11 @@ SzString *sz_string_repeat(const SzString *s, int64_t n); SzString *sz_string_strip_prefix(const SzString *s, const SzString *prefix); /* Drop `suffix` when `s` ends with it. Else copy `s`. */ SzString *sz_string_strip_suffix(const SzString *s, const SzString *suffix); -/* Pad on the left to width `n`. `n` <= len, or empty pad, copies `s`. */ +/* Pad on the left to code-point width `n`. Fill cycles complete code points. + * `n` <= len, or empty pad, copies `s`. */ SzString *sz_string_pad_left(const SzString *s, int64_t n, const SzString *pad); -/* Pad on the right to width `n`. `n` <= len, or empty pad, copies `s`. */ +/* Pad on the right to code-point width `n`. Fill cycles complete code points. + * `n` <= len, or empty pad, copies `s`. */ SzString *sz_string_pad_right(const SzString *s, int64_t n, const SzString *pad); /* 1 when `s` is empty or only ASCII space, tab, CR, LF. */ int64_t sz_string_is_blank(const SzString *s); @@ -433,7 +438,7 @@ struct SzDeferred { int ok; void *value; SzError *error; - void *waiters; /* runtime Fiber* list while get is parked */ + void *waiters; /* FIFO Fiber* list while get is parked */ }; SzDeferred *sz_deferred_make(void); @@ -504,7 +509,7 @@ enum { SZ_ST_ZIPIDX = 17, SZ_ST_FLATTEN = 18, SZ_ST_CHANGES = 19, - SZ_ST_REPEATN = 20, + SZ_ST_RANGE = 20, SZ_ST_FILTERNOT = 21, SZ_ST_MAPCONCAT = 22, SZ_ST_ZIPWITH = 23, @@ -515,7 +520,9 @@ enum { SZ_ST_DROPRIGHT = 28, SZ_ST_FINDLAST = 29, SZ_ST_EVALTAP = 30, - SZ_ST_INTERLEAVE = 31 + SZ_ST_INTERLEAVE = 31, + SZ_ST_ITERATE = 32, + SZ_ST_UNFOLD = 33 }; struct SzStream { @@ -539,7 +546,7 @@ SzStream *sz_stream_find(SzStream *inner, SzStreamPred pred, void *env); SzIo *sz_stream_exists(SzStream *s, SzStreamPred pred, void *env); /* IO[Bool] */ SzStream *sz_stream_take(SzStream *inner, int64_t n); SzStream *sz_stream_drop(SzStream *inner, int64_t n); -/* Boxed ints `[from, until)`. Empty when `until` <= `from`. */ +/* Boxed ints `[from, until)`. Empty when `until` <= `from`. Pulls one box. */ SzStream *sz_stream_range(int64_t from, int64_t until); /* Concat `inner` with itself `n` times. `n` <= 0 is empty. */ SzStream *sz_stream_repeat_n(SzStream *inner, int64_t n); @@ -566,9 +573,9 @@ SzStream *sz_stream_take_right(SzStream *inner, int64_t n); SzStream *sz_stream_drop_right(SzStream *inner, int64_t n); SzStream *sz_stream_find_last(SzStream *inner, SzStreamPred pred, void *env); SzStream *sz_stream_evaltap(SzStream *inner, SzCont f, void *env); -/* Emit `z`, then `f(z)`, n times. n <= 0 is empty. */ +/* Emit `z`, then `f(z)`, n times. n <= 0 is empty. Pulls one value. */ SzStream *sz_stream_iterate(void *z, int64_t n, SzStreamMapFn f, void *env); -/* `f` returns List of 0 or 1 `(A, Z)` pair. Empty stops. Caps at 65536. */ +/* `f` returns List of 0 or 1 `(A, Z)` pair. Empty stops. Cap 65536 pulled steps. */ SzStream *sz_stream_unfold(void *z, SzStreamMapFn f, void *env); SzIo *sz_stream_head(SzStream *s); /* IO[A]; fails when empty */ SzIo *sz_stream_last(SzStream *s); /* IO[A]; fails when empty */ @@ -727,7 +734,8 @@ SzList *sz_list_distinct_by(SzList *xs, SzListMapFn fn, void *env, * pair is skipped. Empty is empty. Key kind is boxed Int or String. */ SzMap *sz_list_to_map(SzList *pairs); /* Set from `List[Int]` or `List[String]`. Duplicate cells collapse. - * Empty is empty. `key_kind` is 0 for boxed Int, 1 for String. */ + * Empty is empty. A non-empty list infers kind from the first cell. + * `key_kind` is unused then. */ SzMap *sz_list_to_set(SzList *xs, int32_t key_kind); /* Cells of `xs` that are missing from `ys`. Empty `xs` is empty. */ SzList *sz_list_diff(SzList *xs, SzList *ys); @@ -760,7 +768,8 @@ int64_t sz_list_segment_length(SzList *xs, SzListPred pred, void *env, int64_t f int64_t sz_list_is_defined_at(SzList *xs, int64_t index); /* Negative when len < n, 0 when equal, positive when len > n. */ int64_t sz_list_length_compare(SzList *xs, int64_t n); -/* `as_int` 1 orders boxed Int, else String. `want_max` 1 is max. Empty max panics. */ +/* Kind comes from the first non-null head (boxed Int or String). + * `as_int` is unused. Empty max panics. */ SzList *sz_list_sort(SzList *xs, int64_t as_int); SzList *sz_list_sort_by(SzList *xs, SzListMapFn fn, void *env); void *sz_list_max(SzList *xs, int64_t as_int); @@ -776,7 +785,8 @@ int64_t sz_list_sum(SzList *xs); int64_t sz_list_product(SzList *xs); /* Release the spine; heads drop through RC. */ void sz_list_free(SzList *xs); -SzString *sz_list_join(const SzList *xs, const char *sep); +/* Join string cells with `sep`. A null cell is empty. Other heads panic. */ +SzString *sz_list_join(const SzList *xs, const SzString *sep); /* Persistent Map / Set (NULL = empty). key_kind 0 = boxed i64, 1 = String. */ struct SzMap { @@ -787,10 +797,12 @@ struct SzMap { int32_t key_kind; }; SzMap *sz_map_empty(void); +/* Empty insert infers kind from `key`. A non-empty tree keeps + * `m->key_kind`. `key_kind` is unused. */ SzMap *sz_map_set(SzMap *m, void *key, void *val, int32_t key_kind); void *sz_map_get_or(SzMap *m, void *key, void *dflt); -/* 0–1 list of the value. Miss is empty. Cons retains the value. */ -SzList *sz_map_get(SzMap *m, void *key); +/* Option of the value. A miss is None. A hit is Some, including null. */ +void *sz_map_get(SzMap *m, void *key); int64_t sz_map_contains(SzMap *m, void *key); /* Drop `key` if present. Missing key retains `m`. */ SzMap *sz_map_remove(SzMap *m, void *key); @@ -820,8 +832,9 @@ int64_t sz_map_exists(SzMap *m, SzListPred pred, void *env); int64_t sz_map_forall(SzMap *m, SzListPred pred, void *env); /* Keep keys that match `pred`. Empty stays empty. */ SzMap *sz_set_filter(SzMap *s, SzListPred pred, void *env); -/* Map each key. Mapper returns +1. `key_kind` is 0 for boxed Int, 1 for - * String. Duplicate keys collapse. Empty stays empty. */ +/* Map each key. Mapper returns +1. Kind comes from the first mapped + * key. `key_kind` is unused then. Duplicate keys collapse. Empty stays + * empty. */ SzMap *sz_set_map(SzMap *s, SzListMapFn fn, void *env, int32_t key_kind); /* 1 when any key matches `pred`. Empty is 0. */ int64_t sz_set_exists(SzMap *s, SzListPred pred, void *env); @@ -898,20 +911,20 @@ SzIo *sz_sys_read_line(void); /* IO[String]: one stdin line; EOF → ""; parks o SzIo *sz_sys_read(int64_t n); /* IO[String]: n stdin bytes (or fewer at EOF); parks on poll */ SzIo *sz_sys_write(SzString *s); /* IO[Unit]: stdout bytes, no newline */ SzIo *sz_sys_exec(SzString *cmd); /* IO[(Int, String, String)] code+stdout+stderr; parks on poll; fails under TestRuntime */ -SzIo *sz_sys_spawn(SzString *cmd); /* IO[Int] pid; stdin/stdout pipes; fails under TestRuntime */ +SzIo *sz_sys_spawn(SzString *cmd); /* IO[Int] pid; stdin/stdout pipes; stderr inherited; fails under TestRuntime */ SzIo *sz_sys_child_write(int64_t pid, SzString *s); /* IO[Unit] child stdin; parks on poll */ -SzIo *sz_sys_child_read(int64_t pid, int64_t n); /* IO[String] child stdout bytes; fewer at EOF; parks on poll */ +SzIo *sz_sys_child_read(int64_t pid, int64_t n); /* IO[String] child stdout bytes; fewer at EOF; unknown pid fails; parks on poll */ SzIo *sz_sys_child_close(int64_t pid); /* IO[Unit] close child stdin */ SzIo *sz_sys_alive(int64_t pid); /* IO[Int] 1 if running */ -SzIo *sz_sys_kill(int64_t pid); /* IO[Unit] SIGTERM; no-op if already gone */ +SzIo *sz_sys_kill(int64_t pid); /* IO[Unit] SIGTERM; waitpid WNOHANG; slot stays until reaped */ SzIo *sz_sys_getenv(SzString *key); /* Blessed Clock / Random / Net (impurity boundary) */ SzIo *sz_clock_real_time(void); /* IO[Int] wall epoch ms */ SzIo *sz_clock_monotonic(void); /* IO[Int] monotonic ms */ -int64_t sz_clock_monotonic_ms_sync(void); /* sync read for UI pump dt */ +int64_t sz_clock_monotonic_ms_sync(void); /* sync monotonic ms (scheduler, Net, UI); TestRuntime fake clock */ -SzIo *sz_random_next_int(int64_t bound); /* IO[Int] in [0, bound) */ +SzIo *sz_random_next_int(int64_t bound); /* IO[Int] in [0, bound); bound <= 0 fails */ SzIo *sz_net_http_get(SzString *url); /* IO[String] body; 2xx; 1 MiB; http:// or https:// */ SzIo *sz_net_http_post(SzString *url, SzString *body); @@ -932,10 +945,12 @@ typedef struct SzNetSock { SzIo *sz_net_tcp_connect(SzString *host, int64_t port); /* IO[SzNetSock] */ SzIo *sz_net_tcp_listen(int64_t port); /* IO[SzNetSock] */ SzIo *sz_net_tcp_accept(SzNetSock *listener); /* IO[SzNetSock] */ -SzIo *sz_net_tcp_read(SzNetSock *conn, int64_t n); /* IO[String] */ +/* At most n bytes (cap 1 MiB). Short read when no more data is ready. */ +SzIo *sz_net_tcp_read(SzNetSock *conn, int64_t n); /* IO[String] */ SzIo *sz_net_tcp_write(SzNetSock *conn, SzString *s); /* IO[Unit] */ SzIo *sz_net_tcp_close(SzNetSock *sock); /* IO[Unit] */ -/* Blessed UDP. Bind 0 picks an ephemeral port. Recv is (host, port, data). */ +/* Blessed UDP. Bind is IPv4 localhost. Bind 0 picks an ephemeral port. + * Recv is (host, port, data). An IPv6 send host fails. */ SzIo *sz_net_udp_bind(int64_t port); /* IO[SzNetSock] */ SzIo *sz_net_udp_send(SzNetSock *sock, SzString *host, int64_t port, SzString *data); @@ -943,6 +958,8 @@ SzIo *sz_net_udp_recv(SzNetSock *sock, int64_t n); /* IO[(String, Int, String)] SzIo *sz_net_udp_close(SzNetSock *sock); void sz_net_sock_on_free(SzNetSock *s); void sz_testrt_net_sock_gone(SzNetSock *s); +/* 4 = IPv4, 6 = IPv6, 0 = not a literal. Writes a canonical host when canon is set. */ +int sz_net_host_family(const char *host, char *canon, size_t canon_cap); /* Test-only: UDP nameserver for live HTTP DNS. NULL ip restores /etc/resolv.conf. */ void sz_net_test_set_nameserver(const char *ipv4, int port); /* Test-only: Host header value for HTTP (RFC 9110). */ @@ -1048,7 +1065,7 @@ void sz_testrt_stdout_append(const char *line); /* appends line + '\n' */ void sz_testrt_stdout_write(const char *bytes, size_t n); /* raw; no extra newline */ const char *sz_testrt_stdout_cstr(void); void sz_testrt_env_set(const char *key, const char *val); /* sealed Sys.getenv map */ -const char *sz_testrt_env_get(const char *key); /* NULL if unset */ +const char *sz_testrt_env_get(const char *key); /* NULL if unset. Install copies SCUZZ_SERVE / SCUZZ_KIT */ void sz_testrt_proc_put(int64_t pid); /* fake Sys.alive table */ int sz_testrt_proc_alive(int64_t pid); /* 1 if registered */ void sz_testrt_proc_kill(int64_t pid); /* drop from table */ @@ -1114,6 +1131,9 @@ void sz_property_session_reset(void); void sz_timeline_set_drive(const char *line); int sz_timeline_replaying(void); int64_t sz_timeline_replay_signal_int(const char *name); +SzString *sz_timeline_replay_signal_str(const char *name); +int64_t sz_timeline_replay_signal_list_len(const char *name); +SzString *sz_timeline_replay_signal_list_at(const char *name, int64_t index); int64_t sz_timeline_len(void *tl); int64_t sz_timeline_signal_int(void *tl, int64_t i, SzString *name); int64_t sz_timeline_signal_list_len(void *tl, int64_t i, SzString *name); diff --git a/crates/runtime/include/scuzz_ui.h b/crates/runtime/include/scuzz_ui.h index 34cef3ce..675b70b9 100644 --- a/crates/runtime/include/scuzz_ui.h +++ b/crates/runtime/include/scuzz_ui.h @@ -126,10 +126,10 @@ void sz_signal_list_set(SzSignalList *s, SzList *v); SzList *sz_signal_list_get(const SzSignalList *s); void sz_signal_list_free(SzSignalList *s); -/* Signal store dump: one "kind[id] = value" line per live signal, in creation - order (fuzz oracle; caller frees SzString). */ +/* Signal store dump: one "kind[id] name = value" line per live signal. + * String values use the editor dump escape dialect. Caller frees SzString. */ SzString *sz_signal_dump(void); -/* Property observation: signal store by creation-order id (TestRuntime / fuzz). */ +/* Publish the for-binder name. Property and Timeline kits read that name. */ void sz_signal_name(const void *sig, const char *name); int64_t sz_property_signal_int(SzString *name); @@ -677,8 +677,9 @@ int sz_ui_session_set_record(SzUiSession *session, const char *path); int sz_ui_session_reload(SzUiSession *session); /* dlopen `path` (copied to a unique sibling so the OS does not keep a stale * image) and set the rebuild factory from exported `sz_ui_reload_rebuild`. - * Signals stay in `rebuild_env`. Does not rebuild until reload/stamp. - * Stamp-watch loads `SCUZZ_UI_RELOAD_CODE` (if set) before rebuild. */ + * Unlink the copy after dlopen. Signals stay in `rebuild_env`. Does not + * rebuild until reload/stamp. Stamp-watch loads `SCUZZ_UI_RELOAD_CODE` (if + * set) before rebuild. */ int sz_ui_session_load_code(SzUiSession *session, const char *path); void sz_ui_unmount(SzUiSession *session); /* Snapshot PNG / structural dump from SCUZZ_SNAPSHOT_PATH / SCUZZ_FUZZ_DUMP. */ diff --git a/crates/runtime/src/deferred.c b/crates/runtime/src/deferred.c index e84c1a91..6f3e4876 100644 --- a/crates/runtime/src/deferred.c +++ b/crates/runtime/src/deferred.c @@ -64,6 +64,8 @@ void sz_deferred_complete_now(SzDeferred *d, void *value) { sz_retain(value); d->value = value; sz_fiber_wake_deferred(d); + if (d->waiters) + sz_panic("sz_deferred_complete_now: waiters remain"); } void sz_deferred_fail_now(SzDeferred *d, SzError *err) { @@ -79,6 +81,8 @@ void sz_deferred_fail_now(SzDeferred *d, SzError *err) { sz_retain(err); d->error = err; sz_fiber_wake_deferred(d); + if (d->waiters) + sz_panic("sz_deferred_fail_now: waiters remain"); } static void *deferred_fail_thunk(void *env) { diff --git a/crates/runtime/src/fs.c b/crates/runtime/src/fs.c index 4156126b..8a1b2248 100644 --- a/crates/runtime/src/fs.c +++ b/crates/runtime/src/fs.c @@ -137,8 +137,7 @@ static void *fs_write_result(void *env) { FsResult *r = (FsResult *)rc_box_zero(sizeof(FsResult)); const char *p = sz_string_cstr(path); FILE *f; - sz_timeline_log_bytes("Fs.write", contents ? contents->data : "", - contents ? contents->len : 0); + sz_timeline_log_cstr("Fs.write", p); f = fopen(p, "wb"); if (!f) { char msg[512]; @@ -198,19 +197,26 @@ static SzList *cons_fs_entry(SzList *acc, const char *name, int is_dir) { return out; } +static int fs_join_into(char *out, size_t out_sz, const char *dir, const char *name) { + int n; + size_t dlen = strlen(dir); + if (dlen == 1 && dir[0] == '/') + n = snprintf(out, out_sz, "/%s", name); + else if (dlen > 0 && dir[dlen - 1] == '/') + n = snprintf(out, out_sz, "%s%s", dir, name); + else if (dlen == 0) + n = snprintf(out, out_sz, "%s", name); + else + n = snprintf(out, out_sz, "%s/%s", dir, name); + return n >= 0 && (size_t)n < out_sz; +} + static int fs_name_is_dir(const char *dir, const char *name) { char full[2048]; struct stat st; - size_t n = strlen(dir); - if (n == 1 && dir[0] == '/') - snprintf(full, sizeof full, "/%s", name); - else if (n > 0 && dir[n - 1] == '/') - snprintf(full, sizeof full, "%s%s", dir, name); - else if (n == 0 || (n == 1 && dir[0] == '.')) - snprintf(full, sizeof full, "%s", name); - else - snprintf(full, sizeof full, "%s/%s", dir, name); - if (stat(full, &st) != 0) + if (!fs_join_into(full, sizeof full, dir, name)) + return 0; + if (lstat(full, &st) != 0) return 0; return S_ISDIR(st.st_mode) ? 1 : 0; } @@ -220,7 +226,9 @@ static void *fs_list_result(void *env) { SzString *path = pack_path(pack); FsResult *r = (FsResult *)rc_box_zero(sizeof(FsResult)); const char *p = sz_string_cstr(path); - DIR *d = opendir(p); + DIR *d; + sz_timeline_log_cstr("Fs.list", p); + d = opendir(p); if (!d) { char msg[512]; snprintf(msg, sizeof(msg), "Fs.list: cannot open %s: %s", p, strerror(errno)); @@ -282,6 +290,12 @@ static void *fs_mkdirs_result(void *env) { const char *p = sz_string_cstr(path); char tmp[1024]; size_t len = strlen(p); + sz_timeline_log_cstr("Fs.mkdirs", p); + if (len == 0 || strcmp(p, ".") == 0 || strcmp(p, "/") == 0) { + r->is_err = 0; + r->as.ok = NULL; + goto done; + } if (len >= sizeof(tmp)) { r->is_err = 1; r->as.err = sz_error_new(2, "Fs.mkdirs: path too long"); @@ -318,7 +332,9 @@ static void *fs_canonicalize_result(void *env) { SzString *path = pack_path(pack); FsResult *r = (FsResult *)rc_box_zero(sizeof(FsResult)); const char *p = sz_string_cstr(path); - char *resolved = realpath(p, NULL); + char *resolved; + sz_timeline_log_cstr("Fs.canonicalize", p); + resolved = realpath(p, NULL); if (!resolved) { char msg[512]; snprintf(msg, sizeof(msg), "Fs.canonicalize: %s: %s", p, strerror(errno)); @@ -350,6 +366,7 @@ static void *fs_exists_result(void *env) { FsResult *r = (FsResult *)rc_box_zero(sizeof(FsResult)); const char *p = sz_string_cstr(path); struct stat st; + sz_timeline_log_cstr("Fs.exists", p); r->is_err = 0; r->as.ok = sz_box_i64(stat(p, &st) == 0 ? 1 : 0); return r; @@ -364,22 +381,74 @@ static SzIo *fs_after_exists(void *value, void *env) { SzIo *sz_fs_exists(SzString *path) { return fs_bind(path, fs_after_exists); } -static int fs_join_into(char *out, size_t out_sz, const char *dir, const char *name) { - int n; - size_t dlen = strlen(dir); - if (dlen == 1 && dir[0] == '/') - n = snprintf(out, out_sz, "/%s", name); - else if (dlen > 0 && dir[dlen - 1] == '/') - n = snprintf(out, out_sz, "%s%s", dir, name); - else if (dlen == 0) - n = snprintf(out, out_sz, "%s", name); - else - n = snprintf(out, out_sz, "%s/%s", dir, name); - return n >= 0 && (size_t)n < out_sz; +/* Collapse duplicate slashes, trailing slashes, and "." segments. + * Keep ".." segments. Writes a NUL-terminated path into out. */ +static int fs_collapse_dot_slash(const char *p, char *out, size_t out_sz) { + size_t i = 0; + size_t o = 0; + int abs = 0; + if (!p) + p = ""; + if (p[0] == '/') { + abs = 1; + if (out_sz < 2) + return 0; + out[o++] = '/'; + while (p[i] == '/') + i++; + } + while (p[i]) { + size_t start = i; + size_t n; + while (p[i] && p[i] != '/') + i++; + n = i - start; + if (n == 1 && p[start] == '.') { + /* skip "." */ + } else if (n > 0) { + if (o > 0 && out[o - 1] != '/') { + if (o + 1 >= out_sz) + return 0; + out[o++] = '/'; + } + if (o + n >= out_sz) + return 0; + memcpy(out + o, p + start, n); + o += n; + } + while (p[i] == '/') + i++; + } + if (o > 1 && out[o - 1] == '/') + o--; + out[o] = '\0'; + if (!abs && o == 0) { + if (out_sz < 2) + return 0; + out[0] = '.'; + out[1] = '\0'; + } + return 1; +} + +static int fs_same_inode(const char *a, const char *b) { + struct stat sa; + struct stat sb; + if (lstat(a, &sa) != 0 || lstat(b, &sb) != 0) + return 0; + return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; } static int fs_is_root_path(const char *p) { - return !p || !p[0] || strcmp(p, ".") == 0 || strcmp(p, "/") == 0; + char collapsed[2048]; + if (!p || !p[0]) + return 1; + if (!fs_collapse_dot_slash(p, collapsed, sizeof collapsed)) + return 0; + if (strcmp(collapsed, ".") == 0 || strcmp(collapsed, "/") == 0 || + strcmp(collapsed, "..") == 0) + return 1; + return fs_same_inode(p, "/") || fs_same_inode(p, "."); } static int fs_parent_is_dir(const char *path) { @@ -465,6 +534,7 @@ static void *fs_delete_result(void *env) { SzString *path = pack_path(pack); FsResult *r = (FsResult *)rc_box_zero(sizeof(FsResult)); const char *p = sz_string_cstr(path); + sz_timeline_log_cstr("Fs.delete", p); if (fs_is_root_path(p)) { r->is_err = 1; r->as.err = sz_error_new(2, "Fs.delete: refused root"); @@ -495,6 +565,7 @@ static void *fs_rename_result(void *env) { const char *src = sz_string_cstr(from); const char *dst = sz_string_cstr(to); struct stat st; + sz_timeline_log_cstr("Fs.rename", src); if (fs_is_root_path(src) || fs_is_root_path(dst)) { r->is_err = 1; r->as.err = sz_error_new(2, "Fs.rename: refused root"); @@ -631,6 +702,7 @@ static void *fs_walk_result(void *env) { const char *p = sz_string_cstr(path); struct stat st; SzList *acc; + sz_timeline_log_cstr("Fs.walk", p); if (stat(p, &st) != 0 || !S_ISDIR(st.st_mode)) { char msg[512]; snprintf(msg, sizeof msg, "Fs.walk: not a directory: %s", p); diff --git a/crates/runtime/src/impurity.c b/crates/runtime/src/impurity.c index 5f077d73..5d7e8fcd 100644 --- a/crates/runtime/src/impurity.c +++ b/crates/runtime/src/impurity.c @@ -48,10 +48,13 @@ static SzIo *do_read_line(void *value, void *env) { static SzIo *after_args(void *value, void *env) { SzList *xs = (SzList *)value; + SzString *sep; SzString *joined; SzIo *io; (void)env; - joined = sz_list_join(xs, ","); + sep = sz_string_from_cstr(","); + joined = sz_list_join(xs, sep); + sz_release(sep); sz_release(xs); io = labeled("args:", joined); sz_release(joined); diff --git a/crates/runtime/src/json.c b/crates/runtime/src/json.c index e5270441..d2d689bb 100644 --- a/crates/runtime/src/json.c +++ b/crates/runtime/src/json.c @@ -54,34 +54,40 @@ static locale_t json_c_locale(void) { static double json_strtod(const char *s, char **end) { locale_t loc = json_c_locale(); + if (!loc) { + if (end) + *end = (char *)s; + return 0.0; + } #ifdef __APPLE__ - if (loc) - return strtod_l(s, end, loc); + return strtod_l(s, end, loc); #else - if (loc) { + { locale_t old = uselocale(loc); double x = strtod(s, end); uselocale(old); return x; } #endif - return strtod(s, end); } static int json_fmt_double(char *tmp, size_t n, double x) { locale_t loc = json_c_locale(); + if (!loc) { + if (n) + tmp[0] = '\0'; + return -1; + } #ifdef __APPLE__ - if (loc) - return snprintf_l(tmp, n, loc, "%.17g", x); + return snprintf_l(tmp, n, loc, "%.17g", x); #else - if (loc) { + { locale_t old = uselocale(loc); int r = snprintf(tmp, n, "%.17g", x); uselocale(old); return r; } #endif - return snprintf(tmp, n, "%.17g", x); } static void *box_f64(double x) { @@ -518,6 +524,8 @@ static SzAdt *jp_value(Jp *p) { SzAdt *sz_json_parse(SzString *s) { Jp p; SzAdt *v; + if (!json_c_locale()) + return result_err("C locale"); if (!s) return result_err("Json.parse(null)"); p.s = sz_string_cstr(s); @@ -682,6 +690,8 @@ static void jb_value(Jb *b, const SzAdt *j) { SzAdt *sz_json_stringify(SzAdt *j) { Jb b; SzString *s; + if (!json_c_locale()) + return result_err("C locale"); if (!j) return result_err("Json.stringify(null)"); memset(&b, 0, sizeof b); diff --git a/crates/runtime/src/list.c b/crates/runtime/src/list.c index ab4cc791..aa521381 100644 --- a/crates/runtime/src/list.c +++ b/crates/runtime/src/list.c @@ -27,6 +27,31 @@ static SzList *sz_list_cons_take(void *head, SzList *tail) { return n; } +/* First non-null head must be boxed Int or String. Empty or all-null panics. */ +static uint32_t list_elem_kind(SzList *xs, const char *msg) { + SzList *p; + uint32_t k; + for (p = xs; p; p = p->tail) { + if (!p->head) + continue; + k = sz_rc_kind(p->head); + if (k == SZ_RC_BOX || k == SZ_RC_STRING) + return k; + sz_panic(msg); + } + sz_panic(msg); +} + +static void list_require_kind(const SzList *xs, uint32_t want, const char *msg) { + const SzList *p; + for (p = xs; p; p = p->tail) { + if (!p->head) + continue; + if (sz_rc_kind(p->head) != want) + sz_panic(msg); + } +} + void *sz_list_head(const SzList *xs) { if (!xs) sz_panic("List.head on empty"); @@ -320,6 +345,7 @@ static SzList *flatten_from_rev(SzList *rev) { SzList *out = NULL; SzList *p; SzList *next; + list_require_kind(rev, SZ_RC_LIST, "List.flatten: not List"); for (p = rev; p; p = p->tail) { next = sz_list_concat((SzList *)p->head, out); sz_release(out); @@ -643,6 +669,7 @@ SzPair *sz_list_unzip(SzList *pairs) { SzList *arev; SzList *brev; SzPair *out; + list_require_kind(pairs, SZ_RC_PAIR, "List.unzip: not pair"); for (p = pairs; p; p = p->tail) { inner = (SzPair *)p->head; if (!inner) @@ -878,9 +905,10 @@ SzMap *sz_list_to_map(SzList *pairs) { SzMap *acc = NULL; SzList *p; int32_t kind = 1; + list_require_kind(pairs, SZ_RC_PAIR, "List.toMap: not pair"); if (pairs && pairs->head) { SzPair *first = (SzPair *)pairs->head; - if (first && first->left) + if (first->left) kind = sz_map_infer_key_kind(first->left); } for (p = pairs; p; p = p->tail) { @@ -898,8 +926,11 @@ SzMap *sz_list_to_map(SzList *pairs) { SzMap *sz_list_to_set(SzList *xs, int32_t key_kind) { SzMap *acc = NULL; SzList *p; + int32_t kind = key_kind; + if (xs && xs->head) + kind = sz_map_infer_key_kind(xs->head); for (p = xs; p; p = p->tail) { - SzMap *next = sz_map_set(acc, p->head, NULL, key_kind); + SzMap *next = sz_map_set(acc, p->head, NULL, kind); sz_release(acc); acc = next; } @@ -1239,8 +1270,12 @@ SzList *sz_list_sort(SzList *xs, int64_t as_int) { SzSortSlot *slots; SzList *p; size_t i; + uint32_t kind; + (void)as_int; if (!xs) return NULL; + kind = list_elem_kind(xs, "List.sort: not Int or String"); + list_require_kind(xs, kind, "List.sort: not Int or String"); slots = (SzSortSlot *)sz_alloc((size_t)n * sizeof(SzSortSlot)); i = 0; for (p = xs; p; p = p->tail) { @@ -1249,7 +1284,8 @@ SzList *sz_list_sort(SzList *xs, int64_t as_int) { slots[i].idx = i; i++; } - return sort_slots(slots, (size_t)n, as_int ? cmp_int_slots : cmp_str_slots); + return sort_slots(slots, (size_t)n, + kind == SZ_RC_BOX ? cmp_int_slots : cmp_str_slots); } SzList *sz_list_sort_by(SzList *xs, SzListMapFn fn, void *env) { @@ -1274,8 +1310,8 @@ SzList *sz_list_sort_by(SzList *xs, SzListMapFn fn, void *env) { return sort_slots(slots, (size_t)n, cmp_key_slots); } -static int cell_ord(void *a, void *b, int64_t as_int) { - if (as_int) { +static int cell_ord(void *a, void *b, uint32_t kind) { + if (kind == SZ_RC_BOX) { int64_t ka = sz_unbox_i64(a); int64_t kb = sz_unbox_i64(b); if (ka < kb) @@ -1291,11 +1327,17 @@ static void *list_extreme(SzList *xs, int64_t as_int, int want_max, const char *empty_msg) { SzList *p; void *best; + uint32_t kind; + const char *bad = + want_max ? "List.max: not Int or String" : "List.min: not Int or String"; + (void)as_int; if (!xs) sz_panic(empty_msg); + kind = list_elem_kind(xs, bad); + list_require_kind(xs, kind, bad); best = xs->head; for (p = xs->tail; p; p = p->tail) { - int c = cell_ord(p->head, best, as_int); + int c = cell_ord(p->head, best, kind); if (want_max ? c > 0 : c < 0) best = p->head; } @@ -1339,6 +1381,7 @@ SzMap *sz_list_group_by(SzList *xs, SzListMapFn fn, void *env, int32_t key_kind) int64_t sz_list_sum(SzList *xs) { uint64_t acc = 0; SzList *p; + list_require_kind(xs, SZ_RC_BOX, "List.sum: not Int"); for (p = xs; p; p = p->tail) acc += (uint64_t)sz_unbox_i64(p->head); return (int64_t)acc; @@ -1347,6 +1390,7 @@ int64_t sz_list_sum(SzList *xs) { int64_t sz_list_product(SzList *xs) { uint64_t acc = 1; SzList *p; + list_require_kind(xs, SZ_RC_BOX, "List.product: not Int"); for (p = xs; p; p = p->tail) acc *= (uint64_t)sz_unbox_i64(p->head); return (int64_t)acc; @@ -1398,12 +1442,22 @@ int sz_list_non_empty(const SzList *xs) { return xs != NULL; } void sz_list_free(SzList *xs) { sz_release(xs); } -SzString *sz_list_join(const SzList *xs, const char *sep) { - if (!sep) - sep = ""; - size_t sep_len = strlen(sep); +SzString *sz_list_join(const SzList *xs, const SzString *sep) { + const char *sep_data = ""; + size_t sep_len = 0; size_t total = 0; size_t count = 0; + char *buf; + size_t off = 0; + size_t i = 0; + SzString *out; + list_require_kind(xs, SZ_RC_STRING, "List.join: not String"); + if (sep) { + if (sz_rc_kind(sep) != SZ_RC_STRING) + sz_panic("List.join: not String"); + sep_data = sep->data ? sep->data : ""; + sep_len = sep->len; + } for (const SzList *p = xs; p; p = p->tail) { SzString *s = (SzString *)p->head; if (s) { @@ -1420,12 +1474,10 @@ SzString *sz_list_join(const SzList *xs, const char *sep) { } if (total == SIZE_MAX) sz_panic("List.join too large"); - char *buf = (char *)sz_alloc(total + 1); - size_t off = 0; - size_t i = 0; + buf = (char *)sz_alloc(total + 1); for (const SzList *p = xs; p; p = p->tail) { if (i > 0 && sep_len) { - memcpy(buf + off, sep, sep_len); + memcpy(buf + off, sep_data, sep_len); off += sep_len; } SzString *s = (SzString *)p->head; @@ -1436,7 +1488,7 @@ SzString *sz_list_join(const SzList *xs, const char *sep) { i++; } buf[off] = '\0'; - SzString *out = sz_string_from_bytes(buf, off); + out = sz_string_from_bytes(buf, off); sz_free(buf); return out; } diff --git a/crates/runtime/src/map.c b/crates/runtime/src/map.c index 8bad15b2..7c31af60 100644 --- a/crates/runtime/src/map.c +++ b/crates/runtime/src/map.c @@ -48,16 +48,19 @@ static SzMap *map_take_right(void *k, void *v, SzMap *l, SzMap *r, int32_t kind) SzMap *sz_map_set(SzMap *m, void *k, void *v, int32_t kind) { int c; + int32_t knd; + (void)kind; if (!m) - return map_node(k, v, NULL, NULL, kind); + return map_node(k, v, NULL, NULL, sz_map_infer_key_kind(k)); + knd = m->key_kind; c = map_cmp(m, k); if (c == 0) - return map_node(k, v, m->left, m->right, m->key_kind); + return map_node(k, v, m->left, m->right, knd); if (c > 0) - return map_take_left(m->key, m->val, sz_map_set(m->left, k, v, kind), m->right, - m->key_kind); - return map_take_right(m->key, m->val, m->left, sz_map_set(m->right, k, v, kind), - m->key_kind); + return map_take_left(m->key, m->val, sz_map_set(m->left, k, v, knd), m->right, + knd); + return map_take_right(m->key, m->val, m->left, sz_map_set(m->right, k, v, knd), + knd); } void *sz_map_get_or(SzMap *m, void *k, void *dflt) { @@ -72,11 +75,16 @@ void *sz_map_get_or(SzMap *m, void *k, void *dflt) { return sz_map_get_or(m->right, k, dflt); } -SzList *sz_map_get(SzMap *m, void *k) { - void *v = sz_map_get_or(m, k, NULL); - if (!v) - return NULL; - return sz_list_cons(v, NULL); +void *sz_map_get(SzMap *m, void *k) { + int c; + if (!m) + return sz_adt_new(0, NULL); + c = map_cmp(m, k); + if (c == 0) + return sz_adt_new(1, m->val); + if (c > 0) + return sz_map_get(m->left, k); + return sz_map_get(m->right, k); } int64_t sz_map_contains(SzMap *m, void *k) { @@ -348,6 +356,8 @@ SzMap *sz_set_map(SzMap *s, SzListMapFn fn, void *env, int32_t key_kind) { SzMap *out = NULL; SzMap *next; void *nk; + int32_t kind = key_kind; + int got = 0; if (!fn) sz_panic("sz_set_map(null fn)"); if (!s) @@ -356,7 +366,11 @@ SzMap *sz_set_map(SzMap *s, SzListMapFn fn, void *env, int32_t key_kind) { for (p = keys; p; p = p->tail) { /* Mapper returns +1. Set retains. Drop the mapper ref. */ nk = fn(p->head, env); - next = sz_map_set(out, nk, NULL, key_kind); + if (!got) { + kind = sz_map_infer_key_kind(nk); + got = 1; + } + next = sz_map_set(out, nk, NULL, kind); sz_release(nk); sz_release(out); out = next; diff --git a/crates/runtime/src/net.c b/crates/runtime/src/net.c index 0d8645e3..031681b7 100644 --- a/crates/runtime/src/net.c +++ b/crates/runtime/src/net.c @@ -881,7 +881,7 @@ void sz_net_test_http_host_header(const char *host, int port, char *out, http_fmt_hosthdr(out, cap, host, port, 80); } -static void http_build_req(HttpSt *st) { +static int http_build_req(HttpSt *st) { char hosthdr[300]; const char *method = st->method[0] ? st->method : "GET"; const char *body = st->req_body ? sz_string_cstr(st->req_body) : ""; @@ -902,8 +902,8 @@ static void http_build_req(HttpSt *st) { hn = snprintf(hdr, sizeof hdr, "%s %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n", method, st->path, hosthdr); - if (hn < 0) - hn = 0; + if (hn < 0 || (size_t)hn >= sizeof hdr) + return 0; nreq = (size_t)hn + (has_body ? blen : 0); st->req = (char *)sz_alloc(nreq + 1); memcpy(st->req, hdr, (size_t)hn); @@ -916,6 +916,7 @@ static void http_build_req(HttpSt *st) { st->acc[0] = '\0'; st->acc_cap = 1; st->total = 0; + return 1; } static SSL_CTX *g_http_ssl_ctx; @@ -1011,7 +1012,11 @@ static void *http_tcp_connect(void *env) { st->fd4 = -1; st->fd6 = -1; } - http_build_req(st); + if (!http_build_req(st)) { + r->is_err = 1; + r->as.err = http_err(st, "request failed"); + return r; + } st->connect_deadline_ms = sz_clock_monotonic_ms_sync() + HE_CONNECT_MS; if (st->he_wait4) st->he_v4_at_ms = sz_clock_monotonic_ms_sync() + HE_A_DELAY_MS; @@ -1025,7 +1030,11 @@ static void *http_tcp_connect(void *env) { return r; } st->fd = fd; - http_build_req(st); + if (!http_build_req(st)) { + r->is_err = 1; + r->as.err = http_err(st, "request failed"); + return r; + } st->connect_deadline_ms = sz_clock_monotonic_ms_sync() + HE_CONNECT_MS; r->is_err = 0; return r; @@ -1914,7 +1923,7 @@ static int serve_bind_v6(int port) { static int serve_accept_wait(int err) { return err == EAGAIN || err == EWOULDBLOCK || err == ECONNABORTED || - err == EMFILE || err == ENFILE || err == EINTR; + err == EINTR; } static void *serve_ensure_listen(void *env) { @@ -2087,6 +2096,12 @@ static void *serve_read_req(void *env) { r->as.err = sz_error_new(6, "Net.serve: expected HTTP request"); return r; } + if (sz_clock_monotonic_ms_sync() >= st->req_deadline_ms) { + r->is_err = 1; + r->drop = 1; + r->as.err = sz_error_new(6, "Net.serve: request timed out"); + return r; + } r->retry = 1; return r; } diff --git a/crates/runtime/src/net_sock.c b/crates/runtime/src/net_sock.c index d330a977..33081751 100644 --- a/crates/runtime/src/net_sock.c +++ b/crates/runtime/src/net_sock.c @@ -12,8 +12,8 @@ #include /* Blessed TCP and UDP. Listen/bind is localhost. Connect takes IPv4/IPv6 - * literals or localhost. No DNS. Fibers park on poll. TestRuntime uses a - * mailbox. Opaque SzNetSock owns the fds. */ + * literals or localhost. UDP bind is IPv4 only. No DNS. Fibers park on poll. + * TestRuntime uses a mailbox. Opaque SzNetSock owns the fds. */ enum { NS_TCP = 1, NS_LISTEN = 2, NS_UDP = 3 }; enum { NS_IO_MS = 1000 }; @@ -147,6 +147,28 @@ static int parse_tcp_host(const char *host, struct sockaddr_storage *ss, return 0; } +int sz_net_host_family(const char *host, char *canon, size_t canon_cap) { + struct sockaddr_storage ss; + socklen_t len = 0; + if (canon && canon_cap) + canon[0] = '\0'; + if (!parse_tcp_host(host, &ss, &len, 1)) + return 0; + if (ss.ss_family == AF_INET) { + struct sockaddr_in *a = (struct sockaddr_in *)&ss; + if (canon && canon_cap) + inet_ntop(AF_INET, &a->sin_addr, canon, (socklen_t)canon_cap); + return 4; + } + if (ss.ss_family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6 *)&ss; + if (canon && canon_cap) + inet_ntop(AF_INET6, &a->sin6_addr, canon, (socklen_t)canon_cap); + return 6; + } + return 0; +} + static int tcp_begin(const struct sockaddr *sa, socklen_t len) { int fd; if (!sa || len == 0) @@ -492,7 +514,7 @@ static void *accept_cleanup(void *env) { static int accept_wait_err(int err) { return err == EAGAIN || err == EWOULDBLOCK || err == ECONNABORTED || - err == EMFILE || err == ENFILE || err == EINTR; + err == EINTR; } static void *accept_try(void *env) { @@ -587,11 +609,39 @@ SzIo *sz_net_tcp_accept(SzNetSock *listener) { } } +static int read_acc_append(ConnOp *op, const char *p, size_t n) { + size_t need; + if (!op || !p || n == 0) + return 1; + need = op->acc_len + n; + if (need > op->acc_cap) { + size_t cap = op->acc_cap ? op->acc_cap : 64; + char *nb; + while (cap < need) + cap *= 2; + if (cap > NS_READ_MAX) + cap = NS_READ_MAX; + if (need > cap) + n = cap - op->acc_len; + if (n == 0) + return 1; + need = op->acc_len + n; + nb = (char *)sz_alloc(cap); + if (op->acc_len) + memcpy(nb, op->acc, op->acc_len); + sz_free(op->acc); + op->acc = nb; + op->acc_cap = cap; + } + memcpy(op->acc + op->acc_len, p, n); + op->acc_len += n; + return 1; +} + static void *read_try(void *env) { ConnOp *op = (ConnOp *)env; SockResult *r = (SockResult *)rc_box_zero(sizeof(SockResult)); char buf[4096]; - ssize_t n; size_t want; if (!op->sock || op->sock->fd < 0) { r->is_err = 1; @@ -605,22 +655,41 @@ static void *read_try(void *env) { } if (want > NS_READ_MAX) want = NS_READ_MAX; - n = read(op->sock->fd, buf, want < sizeof buf ? want : sizeof buf); - if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - if (sz_clock_monotonic_ms_sync() >= op->deadline_ms) { - r->is_err = 1; - r->as.err = sz_error_new(6, "Net.tcpRead: timed out"); + for (;;) { + size_t room; + ssize_t n; + if (op->acc_len >= want) + break; + room = want - op->acc_len; + if (room > sizeof buf) + room = sizeof buf; + n = read(op->sock->fd, buf, room); + if (n > 0) { + read_acc_append(op, buf, (size_t)n); + continue; + } + if (n == 0) { + r->as.ok = sz_string_from_bytes(op->acc ? op->acc : "", op->acc_len); + return r; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (op->acc_len > 0) { + r->as.ok = sz_string_from_bytes(op->acc, op->acc_len); + return r; + } + if (sz_clock_monotonic_ms_sync() >= op->deadline_ms) { + r->is_err = 1; + r->as.err = sz_error_new(6, "Net.tcpRead: timed out"); + return r; + } + r->retry = 1; return r; } - r->retry = 1; - return r; - } - if (n < 0) { r->is_err = 1; r->as.err = sz_error_new(6, "Net.tcpRead: read failed"); return r; } - r->as.ok = sz_string_from_bytes(buf, (size_t)n); + r->as.ok = sz_string_from_bytes(op->acc, op->acc_len); return r; } diff --git a/crates/runtime/src/random.c b/crates/runtime/src/random.c index 602d3d79..31fc7309 100644 --- a/crates/runtime/src/random.c +++ b/crates/runtime/src/random.c @@ -4,7 +4,8 @@ #include #include -/* Blessed Random — live entropy or TestRuntime seeded LCG. */ +/* Blessed Random. Live seeds once from /dev/urandom then LCG. + * TestRuntime is a seeded LCG. */ static int g_fake = 0; static uint64_t g_state = 1; @@ -50,25 +51,24 @@ static uint64_t next_u64(void) { static void *random_next_thunk(void *env) { int64_t bound = sz_unbox_i64(env); + uint64_t u; + uint64_t lim; int64_t n; sz_timeline_log_cstr("Random.nextInt", ""); - if (bound <= 0) - n = 0; - else { - uint64_t lim = (uint64_t)bound; - uint64_t zone = UINT64_MAX - (UINT64_MAX % lim); - uint64_t u; - do { - u = next_u64(); - } while (u >= zone); - n = (int64_t)(u % lim); - } + lim = (uint64_t)bound; + u = next_u64(); + /* Lemire 2019: map through high bits. LCG bit 0 has period 2. */ + n = (int64_t)(((__uint128_t)u * lim) >> 64); return sz_box_i64(n); } SzIo *sz_random_next_int(int64_t bound) { - void *b = sz_box_i64(bound); - SzIo *io = sz_io_delay(random_next_thunk, b); + void *b; + SzIo *io; + if (bound <= 0) + return sz_io_fail_cstr("Random.nextInt: bound <= 0"); + b = sz_box_i64(bound); + io = sz_io_delay(random_next_thunk, b); sz_release(b); return io; } diff --git a/crates/runtime/src/resource.c b/crates/runtime/src/resource.c index 1720aa86..c0115c4f 100644 --- a/crates/runtime/src/resource.c +++ b/crates/runtime/src/resource.c @@ -10,6 +10,8 @@ typedef struct LangResSt { void *acquired; } LangResSt; +/* Acquire hands one retain to st->acquired. Use and release neths borrow + * that pointer. This drop is the only release of the acquired value. */ static SzIo *lang_fin_free_ok(void *ignored, void *env) { LangResSt *st = (LangResSt *)env; sz_release(ignored); diff --git a/crates/runtime/src/rt_util.h b/crates/runtime/src/rt_util.h index d05314bb..844f7d30 100644 --- a/crates/runtime/src/rt_util.h +++ b/crates/runtime/src/rt_util.h @@ -53,4 +53,121 @@ static inline SzString *pack_path(void *env) { return pack ? (SzString *)pack->left : NULL; } +/* Grow a C string buffer. Used by signal dump, a11y dump, and inject read. */ +static inline void sz_dump_append(char **buf, size_t *len, size_t *cap, + const char *s) { + size_t n = strlen(s); + if (*len + n + 1 > *cap) { + size_t ncap = *cap ? *cap : 256; + char *nb; + while (*len + n + 1 > ncap) + ncap *= 2; + nb = (char *)sz_alloc(ncap); + if (*buf) { + memcpy(nb, *buf, *len); + sz_free(*buf); + } + *buf = nb; + *cap = ncap; + } + memcpy(*buf + *len, s, n); + *len += n; + (*buf)[*len] = '\0'; +} + +/* Editor dump dialect: \\ \" \n \r \t. No surrounding quotes. */ +static inline void sz_dump_append_escaped(char **buf, size_t *len, size_t *cap, + const char *s) { + const char *p; + if (!s) + return; + for (p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '\\') + sz_dump_append(buf, len, cap, "\\\\"); + else if (c == '"') + sz_dump_append(buf, len, cap, "\\\""); + else if (c == '\n') + sz_dump_append(buf, len, cap, "\\n"); + else if (c == '\r') + sz_dump_append(buf, len, cap, "\\r"); + else if (c == '\t') + sz_dump_append(buf, len, cap, "\\t"); + else { + char t[2]; + t[0] = (char)c; + t[1] = '\0'; + sz_dump_append(buf, len, cap, t); + } + } +} + +static inline unsigned char sz_dump_unescape_char(const char **p) { + const char *s = *p; + unsigned char c; + if (*s == '\\' && s[1]) { + s++; + if (*s == 'n') + c = '\n'; + else if (*s == 'r') + c = '\r'; + else if (*s == 't') + c = '\t'; + else + c = (unsigned char)*s; + s++; + *p = s; + return c; + } + c = (unsigned char)*s; + *p = s + 1; + return c; +} + +/* Unescape a dump/script payload. Caller frees. */ +static inline char *sz_dump_unescape(const char *s) { + char *buf = NULL; + size_t len = 0, cap = 0; + const char *p = s ? s : ""; + while (*p) { + char t[2]; + t[0] = (char)sz_dump_unescape_char(&p); + t[1] = '\0'; + sz_dump_append(&buf, &len, &cap, t); + } + if (!buf) + sz_dump_append(&buf, &len, &cap, ""); + return buf; +} + +/* Parse `"…"` with the dump escape dialect. p points at the opening quote. + * Returns the pointer after the closing quote, or NULL. When out is set, + * writes the unescaped bytes (caller frees). */ +static inline const char *sz_dump_parse_quoted(const char *p, char **out) { + char *buf = NULL; + size_t len = 0, cap = 0; + if (out) + *out = NULL; + if (!p || *p != '"') + return NULL; + p++; + while (*p && *p != '"') { + char t[2]; + t[0] = (char)sz_dump_unescape_char(&p); + t[1] = '\0'; + if (out) + sz_dump_append(&buf, &len, &cap, t); + } + if (*p != '"') { + sz_free(buf); + return NULL; + } + if (out) { + if (!buf) + sz_dump_append(&buf, &len, &cap, ""); + *out = buf; + } + return p + 1; +} + #endif diff --git a/crates/runtime/src/runtime.c b/crates/runtime/src/runtime.c index 681186a4..7744c8d8 100644 --- a/crates/runtime/src/runtime.c +++ b/crates/runtime/src/runtime.c @@ -11,8 +11,9 @@ #include #include #include - +#include #if defined(__APPLE__) +#include #include #endif @@ -459,6 +460,12 @@ void sz_retain(void *ptr) { sz_rc_hdr(ptr)->rc += 1; } +uint32_t sz_rc_kind(const void *ptr) { + if (!sz_is_rc(ptr)) + return SZ_RC_KIND_COUNT; + return sz_rc_hdr(ptr)->kind; +} + void sz_release(void *ptr) { SzRcHdr *h; uint32_t kind; @@ -642,11 +649,12 @@ void sz_release(void *ptr) { tag == SZ_ST_ZIPALL || tag == SZ_ST_ORELSE) sz_release(right); /* TAKE/DROP/GROUPED/SLIDING/TAKERIGHT/DROPRIGHT store a count in env. + * RANGE stores from in env and until in right. Those are not RC. * Filter, map, scan, and flatMap nodes store a capture. Function * pointers in right are not RC. */ if (tag != SZ_ST_TAKE && tag != SZ_ST_DROP && tag != SZ_ST_GROUPED && tag != SZ_ST_SLIDING && tag != SZ_ST_TAKERIGHT && - tag != SZ_ST_DROPRIGHT) + tag != SZ_ST_DROPRIGHT && tag != SZ_ST_RANGE) sz_release(env); return; } @@ -702,6 +710,8 @@ void sz_release(void *ptr) { int bad = d->completed && !d->ok; void *v = d->value; SzError *err = d->error; + if (d->waiters) + sz_panic("sz_deferred_free: waiters remain"); d->value = NULL; d->error = NULL; sz_rc_retire(ptr); @@ -758,6 +768,8 @@ void sz_release(void *ptr) { /* --- strings ------------------------------------------------------------- */ +static uint32_t utf8_decode(const char *p, size_t left, size_t *used); + SzString *sz_string_from_bytes(const char *bytes, size_t len) { SzString *s = (SzString *)sz_rc_alloc(sizeof(SzString), SZ_RC_STRING); size_t i; @@ -770,12 +782,16 @@ SzString *sz_string_from_bytes(const char *bytes, size_t len) { s->ulen = 0; s->cp_hint = 0; s->off_hint = 0; - for (i = 0; i < len; i++) { + /* Count with the same walk as utf8_decode so a lone continuation is one + * code point, not invisible. */ + for (i = 0; i < len; ) { + size_t used; unsigned char c = (unsigned char)s->data[i]; if (c >= 0x80) s->is_ascii = 0; - if ((c & 0xC0) != 0x80) - s->ulen++; + utf8_decode(s->data + i, len - i, &used); + i += used; + s->ulen++; } return s; } @@ -795,13 +811,17 @@ void sz_string_free(SzString *s) { sz_release(s); } SzString *sz_string_concat(const SzString *a, const SzString *b) { size_t al = a && a->data ? a->len : 0; size_t bl = b && b->data ? b->len : 0; - char *buf = (char *)sz_alloc(al + bl + 1); + char *buf; + SzString *out; + if (bl > SIZE_MAX - al) + sz_panic("Str.concat too large"); + buf = (char *)sz_alloc(al + bl + 1); if (al) memcpy(buf, a->data, al); if (bl) memcpy(buf + al, b->data, bl); buf[al + bl] = '\0'; - SzString *out = sz_string_from_bytes(buf, al + bl); + out = sz_string_from_bytes(buf, al + bl); sz_free(buf); return out; } @@ -811,14 +831,19 @@ static void builder_append_bytes(SzBuilder *b, const char *p, size_t n) { char *d; if (!b || n == 0) return; + if (n > SIZE_MAX - b->len) + sz_panic("Builder.append too large"); if (b->len + n <= b->cap) { memcpy(b->data + b->len, p, n); b->len += n; return; } cap = b->cap ? b->cap : 64; - while (cap < b->len + n) + while (cap < b->len + n) { + if (cap > SIZE_MAX / 2) + sz_panic("Builder.append too large"); cap *= 2; + } d = (char *)sz_alloc(cap); if (b->len) memcpy(d, b->data, b->len); @@ -917,14 +942,40 @@ SzString *sz_string_from_int(int64_t n) { return sz_string_from_cstr(buf); } +SzString *sz_string_from_bool(int64_t b) { + return sz_string_from_cstr(b ? "true" : "false"); +} + +static locale_t str_c_locale(void) { + static locale_t loc; + if (!loc) + loc = newlocale(LC_ALL_MASK, "C", (locale_t)0); + return loc; +} + SzString *sz_string_from_float(double x) { - char buf[64]; + char buf[512]; char *dot; char *end; + int n; + locale_t loc; if (x != x) { return sz_string_from_cstr("NaN"); } - snprintf(buf, sizeof(buf), "%.6f", x); + loc = str_c_locale(); + if (!loc) + sz_panic("Str.fromFloat: C locale"); +#ifdef __APPLE__ + n = snprintf_l(buf, sizeof(buf), loc, "%.6f", x); +#else + { + locale_t old = uselocale(loc); + n = snprintf(buf, sizeof(buf), "%.6f", x); + uselocale(old); + } +#endif + if (n < 0 || (size_t)n >= sizeof(buf)) + sz_panic("Str.fromFloat overflow"); dot = strchr(buf, '.'); if (dot) { end = buf + strlen(buf); @@ -1072,15 +1123,16 @@ static int64_t utf8_cp_off(const SzString *s, int64_t cp) { i = 0; k = 0; } - for (; i < s->len; i++) { - if (!utf8_cont((unsigned char)s->data[i])) { - if (k == cp) { - mut->cp_hint = cp; - mut->off_hint = (int64_t)i; - return (int64_t)i; - } - k++; + while (i < s->len) { + size_t used; + if (k == cp) { + mut->cp_hint = cp; + mut->off_hint = (int64_t)i; + return (int64_t)i; } + utf8_decode(s->data + i, s->len - i, &used); + i += used; + k++; } if (k == cp) { mut->cp_hint = cp; @@ -1186,11 +1238,19 @@ SzString *sz_string_ureverse(const SzString *s) { /* Code-point count of the byte range [0, byte_end). */ static int64_t utf8_cp_before(const SzString *s, int64_t byte_end) { - int64_t i; + size_t i = 0; int64_t k = 0; - for (i = 0; i < byte_end && i < (int64_t)s->len; i++) { - if (!utf8_cont((unsigned char)s->data[i])) - k++; + size_t end; + if (byte_end <= 0) + return 0; + end = (size_t)byte_end; + if (end > s->len) + end = s->len; + while (i < end) { + size_t used; + utf8_decode(s->data + i, s->len - i, &used); + k++; + i += used; } return k; } @@ -1472,32 +1532,68 @@ SzString *sz_string_strip_suffix(const SzString *s, const SzString *suffix) { } static SzString *sz_string_pad(const SzString *s, int64_t n, const SzString *pad, int left) { - size_t slen = s && s->data ? s->len : 0; - size_t plen = pad && pad->data ? pad->len : 0; + int64_t slen = sz_string_ulen(s); + int64_t plen = sz_string_ulen(pad); + size_t sbytes = s && s->data ? s->len : 0; + size_t pbytes = pad && pad->data ? pad->len : 0; const char *src = s && s->data ? s->data : ""; const char *psrc = pad && pad->data ? pad->data : ""; - size_t want; - size_t need; + int64_t need; + size_t fill_bytes; + size_t po; + size_t off; + int64_t k; char *buf; SzString *out; - size_t i; - if (n <= 0 || (uint64_t)n <= (uint64_t)slen) + size_t want; + if (n <= 0 || n <= slen) return sz_string_copy(s); - if (plen == 0) + if (plen <= 0) return sz_string_copy(s); - if ((uint64_t)n > (uint64_t)SIZE_MAX) + need = n - slen; + if ((uint64_t)need > (uint64_t)SIZE_MAX / 4) + sz_panic("Str.pad too large"); + fill_bytes = 0; + po = 0; + for (k = 0; k < need; k++) { + size_t used; + if (po >= pbytes) + po = 0; + utf8_decode(psrc + po, pbytes - po, &used); + if (used > SIZE_MAX - fill_bytes) + sz_panic("Str.pad too large"); + fill_bytes += used; + po += used; + } + if (fill_bytes > SIZE_MAX - sbytes) sz_panic("Str.pad too large"); - want = (size_t)n; - need = want - slen; + want = fill_bytes + sbytes; buf = (char *)sz_alloc(want + 1); + po = 0; if (left) { - for (i = 0; i < need; i++) - buf[i] = psrc[i % plen]; - memcpy(buf + need, src, slen); + off = 0; + for (k = 0; k < need; k++) { + size_t used; + if (po >= pbytes) + po = 0; + utf8_decode(psrc + po, pbytes - po, &used); + memcpy(buf + off, psrc + po, used); + off += used; + po += used; + } + memcpy(buf + fill_bytes, src, sbytes); } else { - memcpy(buf, src, slen); - for (i = 0; i < need; i++) - buf[slen + i] = psrc[i % plen]; + memcpy(buf, src, sbytes); + off = sbytes; + for (k = 0; k < need; k++) { + size_t used; + if (po >= pbytes) + po = 0; + utf8_decode(psrc + po, pbytes - po, &used); + memcpy(buf + off, psrc + po, used); + off += used; + po += used; + } } buf[want] = '\0'; out = sz_string_from_bytes(buf, want); @@ -2629,8 +2725,11 @@ static void queue_waiter_remove(SzQueue *q, Fiber *f) { } static void def_waiter_add(SzDeferred *d, Fiber *f) { - f->wait_next = (Fiber *)d->waiters; - d->waiters = f; + Fiber **pp = (Fiber **)&d->waiters; + f->wait_next = NULL; + while (*pp) + pp = &(*pp)->wait_next; + *pp = f; } static void def_waiter_remove(SzDeferred *d, Fiber *f) { @@ -3640,6 +3739,26 @@ static int idle_advance(Sched *s) { return 1; continue; } + if (sz_testrt_clock_is_fake()) { + /* Do not poll with a wall timeout. Virtual time does not move. */ + pr = poll(pfds, (nfds_t)npoll, 0); + if (pr < 0 && errno == EINTR) + continue; + now = sz_clock_monotonic_ms_sync(); + if (pr > 0 && wake_pollers(s, pfds, fibs, npoll)) + return 1; + if (wake_sleepers(s, now)) + return 1; + if (pr < 0) + return 0; + if (next < 0) + return 0; + delta = next - now; + if (delta > 0) + sz_testrt_clock_advance(delta); + now = sz_clock_monotonic_ms_sync(); + return wake_sleepers(s, now); + } if (next < 0) timeout_ms = -1; else { @@ -3694,6 +3813,11 @@ static void sched_free_fibers(Sched *s) { sz_release(f->qwait); f->qwait = NULL; } + if (f->dwait) { + def_waiter_remove(f->dwait, f); + sz_release(f->dwait); + f->dwait = NULL; + } cont_free_all(f->stack); f->stack = NULL; fiber_release_result(f); diff --git a/crates/runtime/src/signal.c b/crates/runtime/src/signal.c index f35ba856..a6a70006 100644 --- a/crates/runtime/src/signal.c +++ b/crates/runtime/src/signal.c @@ -54,16 +54,24 @@ static void sig_register(SigKind kind, const void *sig) { g_sig_tail = r; } -/* Publish the author-facing name of a signal (its `for` binder name). */ +/* Publish the author-facing name of a signal (its `for` binder name). + * Last non-empty name wins: a later bind clears the same name on others. */ void sz_signal_name(const void *sig, const char *name) { SigReg *r; + SigReg *mine = NULL; + const char *n = name ? name : ""; for (r = g_sig_head; r; r = r->next) { - if (r->sig == sig) { + if (r->sig == sig) + mine = r; + else if (n[0] && r->name && strcmp(r->name, n) == 0) { sz_free(r->name); - r->name = sz_strdup(name); - return; + r->name = sz_strdup(""); } } + if (!mine) + return; + sz_free(mine->name); + mine->name = sz_strdup(n); } static void sig_unregister(const void *sig) { @@ -108,26 +116,6 @@ static void sig_set_elem_str(const void *sig, int64_t elem_str) { } } -static void dump_append(char **buf, size_t *len, size_t *cap, const char *s) { - size_t n = strlen(s); - if (*len + n + 1 > *cap) { - size_t ncap = *cap ? *cap : 256; - char *nb; - while (*len + n + 1 > ncap) - ncap *= 2; - nb = (char *)sz_alloc(ncap); - if (*buf) { - memcpy(nb, *buf, *len); - sz_free(*buf); - } - *buf = nb; - *cap = ncap; - } - memcpy(*buf + *len, s, n); - *len += n; - (*buf)[*len] = '\0'; -} - SzString *sz_signal_dump(void) { char *buf = NULL; size_t len = 0, cap = 0; @@ -135,7 +123,7 @@ SzString *sz_signal_dump(void) { char tag[256]; SigReg *r; SzString *out; - dump_append(&buf, &len, &cap, ""); + sz_dump_append(&buf, &len, &cap, ""); for (r = g_sig_head; r; r = r->next) { if (r->name && r->name[0]) snprintf(tag, sizeof tag, "%s ", r->name); @@ -145,30 +133,33 @@ SzString *sz_signal_dump(void) { case SIG_INT: snprintf(line, sizeof line, "int[%d] %s= %lld\n", r->id, tag, (long long)sz_signal_int_get((const SzSignalInt *)r->sig)); - dump_append(&buf, &len, &cap, line); + sz_dump_append(&buf, &len, &cap, line); break; case SIG_STR: - snprintf(line, sizeof line, "str[%d] %s= \"%s\"\n", r->id, tag, - sz_signal_str_get((const SzSignalStr *)r->sig)); - dump_append(&buf, &len, &cap, line); + snprintf(line, sizeof line, "str[%d] %s= \"", r->id, tag); + sz_dump_append(&buf, &len, &cap, line); + sz_dump_append_escaped(&buf, &len, &cap, + sz_signal_str_get((const SzSignalStr *)r->sig)); + sz_dump_append(&buf, &len, &cap, "\"\n"); break; case SIG_LIST: { SzList *p = sz_signal_list_get((const SzSignalList *)r->sig); if (!r->elem_str) { snprintf(line, sizeof line, "list[%d] %s= <%lld>\n", r->id, tag, (long long)sz_list_len(p)); - dump_append(&buf, &len, &cap, line); + sz_dump_append(&buf, &len, &cap, line); break; } snprintf(line, sizeof line, "list[%d] %s= [", r->id, tag); - dump_append(&buf, &len, &cap, line); + sz_dump_append(&buf, &len, &cap, line); for (; p; p = p->tail) { const SzString *s = (const SzString *)p->head; - snprintf(line, sizeof line, "\"%s\"%s", s ? sz_string_cstr(s) : "", - p->tail ? ", " : ""); - dump_append(&buf, &len, &cap, line); + sz_dump_append(&buf, &len, &cap, "\""); + sz_dump_append_escaped(&buf, &len, &cap, + s ? sz_string_cstr(s) : ""); + sz_dump_append(&buf, &len, &cap, p->tail ? "\", " : "\""); } - dump_append(&buf, &len, &cap, "]\n"); + sz_dump_append(&buf, &len, &cap, "]\n"); break; } } @@ -188,7 +179,11 @@ int64_t sz_property_signal_int(SzString *name) { } SzString *sz_property_signal_str(SzString *name) { - SigReg *r = sig_find(SIG_STR, name ? sz_string_cstr(name) : ""); + const char *n = name ? sz_string_cstr(name) : ""; + SigReg *r; + if (sz_timeline_replaying()) + return sz_timeline_replay_signal_str(n); + r = sig_find(SIG_STR, n); if (r) return sz_string_from_cstr( sz_signal_str_get((const SzSignalStr *)r->sig)); @@ -196,7 +191,11 @@ SzString *sz_property_signal_str(SzString *name) { } int64_t sz_property_signal_list_len(SzString *name) { - SigReg *r = sig_find(SIG_LIST, name ? sz_string_cstr(name) : ""); + const char *n = name ? sz_string_cstr(name) : ""; + SigReg *r; + if (sz_timeline_replaying()) + return sz_timeline_replay_signal_list_len(n); + r = sig_find(SIG_LIST, n); if (r) return (int64_t)sz_list_len( sz_signal_list_get((const SzSignalList *)r->sig)); @@ -207,9 +206,12 @@ SzString *sz_property_signal_list_at(SzString *name, int64_t index) { SigReg *r; const SzList *p; int64_t i; + const char *n = name ? sz_string_cstr(name) : ""; if (index < 0) return sz_string_from_cstr(""); - r = sig_find(SIG_LIST, name ? sz_string_cstr(name) : ""); + if (sz_timeline_replaying()) + return sz_timeline_replay_signal_list_at(n, index); + r = sig_find(SIG_LIST, n); if (r && r->elem_str) { p = sz_signal_list_get((const SzSignalList *)r->sig); i = 0; diff --git a/crates/runtime/src/stream.c b/crates/runtime/src/stream.c index e4912741..0cd37392 100644 --- a/crates/runtime/src/stream.c +++ b/crates/runtime/src/stream.c @@ -107,16 +107,10 @@ SzStream *sz_stream_drop(SzStream *inner, int64_t n) { } SzStream *sz_stream_range(int64_t from, int64_t until) { - SzStream *s = sz_stream_nil(); - int64_t i; if (until <= from) - return s; - for (i = until - 1; i >= from; i--) { - void *box = sz_box_i64(i); - SzStream *cons = st_new(SZ_ST_CONS, box, s, NULL); - s = cons; - } - return s; + return sz_stream_nil(); + return st_new(SZ_ST_RANGE, NULL, (void *)(intptr_t)until, + (void *)(intptr_t)from); } SzStream *sz_stream_repeat_n(SzStream *inner, int64_t n) { @@ -240,83 +234,28 @@ SzStream *sz_stream_evaltap(SzStream *inner, SzCont f, void *env) { return st_new(SZ_ST_EVALTAP, st_keep(inner), (void *)f, env); } +#define SZ_UNFOLD_CAP 65536 + SzStream *sz_stream_iterate(void *z, int64_t n, SzStreamMapFn f, void *env) { - SzList *nf; - SzList *of; - SzStream *s; - void *cur; - int64_t i; + void *nb; + SzPair *pack; if (!f) sz_panic("sz_stream_iterate(null fn)"); if (n <= 0) return sz_stream_nil(); sz_retain(z); - cur = z; - nf = sz_list_nil(); - for (i = 0; i < n; i++) { - SzList *next_list = sz_list_cons(cur, nf); - sz_release(nf); - nf = next_list; - if (i + 1 < n) { - void *next = f(cur, env); - sz_release(cur); - cur = next; - } - } - sz_release(cur); - of = sz_list_reverse(nf); - sz_release(nf); - s = sz_stream_emits(of); - sz_release(of); - return s; + nb = sz_box_i64(n); + pack = sz_pair_new(nb, env); + sz_release(nb); + return st_new(SZ_ST_ITERATE, z, (void *)f, pack); } -#define SZ_UNFOLD_CAP 65536 - SzStream *sz_stream_unfold(void *z, SzStreamMapFn f, void *env) { - SzList *nf; - SzList *of; - SzStream *s; - void *state; - int64_t n; if (!f) sz_panic("sz_stream_unfold(null fn)"); sz_retain(z); - state = z; - nf = sz_list_nil(); - n = 0; - for (;;) { - SzList *step = (SzList *)f(state, env); - SzPair *p; - void *a; - void *next; - SzList *next_list; - if (sz_list_is_empty(step)) { - sz_release(step); - break; - } - p = (SzPair *)sz_list_head(step); - a = sz_pair_left(p); - next = sz_pair_right(p); - sz_retain(a); - sz_retain(next); - next_list = sz_list_cons(a, nf); - sz_release(nf); - nf = next_list; - sz_release(a); - sz_release(state); - state = next; - sz_release(step); - n += 1; - if (n >= SZ_UNFOLD_CAP) - sz_panic("Stream.unfold: did not terminate"); - } - sz_release(state); - of = sz_list_reverse(nf); - sz_release(nf); - s = sz_stream_emits(of); - sz_release(of); - return s; + sz_retain(env); + return st_new(SZ_ST_UNFOLD, z, (void *)f, env); } typedef struct StEval { @@ -411,6 +350,13 @@ static SzIo *dropwhile_into(SzStream *s, SzList *acc, int64_t remain, SzStreamPred pred, void *penv); static SzIo *after_or_else(void *acc, void *env); static SzIo *after_tap_inner(void *inner_acc, void *env); +static SzIo *mapconcat_into(SzStream *s, SzList *acc, int64_t remain, + SzStreamMapFn f, void *fenv); +static SzIo *changes_into(SzStream *s, SzList *acc, int64_t remain, void *prev, + int have); +static SzIo *flatmap_into(SzStream *s, SzList *acc, int64_t remain, + SzStreamMapFn f, void *fenv); +static int64_t filter_not_pred(void *v, void *env); static int64_t remain_dec(int64_t remain) { return remain < 0 ? remain : remain - 1; @@ -464,6 +410,7 @@ static SzIo *after_or_else(void *acc, void *env) { int64_t added; sz_free(st); added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + /* Empty left is not a fail. Pull the right stream. */ if (added > 0) return pure_drop(acc); return compile_into(right, (SzList *)acc, remain); @@ -920,7 +867,7 @@ static SzIo *filter_into(SzStream *s, SzList *acc, int64_t remain, st->penv = penv; st->remain = remain; st->acc_len = (int64_t)sz_list_len(acc); - return fm_drop(compile_into(s, acc, -1), after_filter, st); + return fm_drop(compile_into(s, acc, remain), after_filter, st); } } } @@ -1057,6 +1004,74 @@ static SzIo *compile_into(SzStream *s, SzList *acc, int64_t remain) { s = (SzStream *)s->right; remain = remain_dec(remain); break; + case SZ_ST_RANGE: { + int64_t from = (int64_t)(intptr_t)s->env; + int64_t until = (int64_t)(intptr_t)s->right; + while (from < until && remain != 0) { + void *box = sz_box_i64(from); + acc = cons_take(box, acc); + sz_release(box); + from++; + remain = remain_dec(remain); + } + return pure_drop(acc); + } + case SZ_ST_ITERATE: { + SzPair *pack = (SzPair *)s->env; + int64_t n = pack ? sz_unbox_i64(pack->left) : 0; + void *fenv = pack ? pack->right : NULL; + SzStreamMapFn f = (SzStreamMapFn)s->right; + void *cur; + int64_t i; + if (n <= 0) + return pure_drop(acc); + sz_retain(s->left); + cur = s->left; + for (i = 0; i < n && remain != 0; i++) { + acc = cons_take(cur, acc); + remain = remain_dec(remain); + if (i + 1 < n && remain != 0) { + void *next = f(cur, fenv); + sz_release(cur); + cur = next; + } + } + sz_release(cur); + return pure_drop(acc); + } + case SZ_ST_UNFOLD: { + SzStreamMapFn f = (SzStreamMapFn)s->right; + void *state; + int64_t steps = 0; + sz_retain(s->left); + state = s->left; + while (remain != 0) { + SzList *step = (SzList *)f(state, s->env); + SzPair *p; + void *a; + void *next; + if (sz_list_is_empty(step)) { + sz_release(step); + break; + } + p = (SzPair *)sz_list_head(step); + a = sz_pair_left(p); + next = sz_pair_right(p); + sz_retain(a); + sz_retain(next); + sz_release(step); + acc = cons_take(a, acc); + sz_release(a); + sz_release(state); + state = next; + remain = remain_dec(remain); + steps += 1; + if (steps >= SZ_UNFOLD_CAP) + sz_panic("Stream.unfold: did not terminate"); + } + sz_release(state); + return pure_drop(acc); + } case SZ_ST_EVAL: { StEval *st = (StEval *)sz_alloc(sizeof(StEval)); st->tail = (SzStream *)s->right; @@ -1118,17 +1133,24 @@ static SzIo *compile_into(SzStream *s, SzList *acc, int64_t remain) { case SZ_ST_ZIPIDX: case SZ_ST_INTERSPERSE: case SZ_ST_GROUPED: - case SZ_ST_FLATTEN: - case SZ_ST_CHANGES: case SZ_ST_SCAN: - case SZ_ST_FLATMAP: - case SZ_ST_FILTERNOT: - case SZ_ST_MAPCONCAT: case SZ_ST_SLIDING: case SZ_ST_TAKERIGHT: case SZ_ST_DROPRIGHT: case SZ_ST_FINDLAST: return lift_into(s, acc, remain); + case SZ_ST_FLATTEN: + return mapconcat_into((SzStream *)s->left, acc, remain, NULL, NULL); + case SZ_ST_MAPCONCAT: + return mapconcat_into((SzStream *)s->left, acc, remain, + (SzStreamMapFn)s->right, s->env); + case SZ_ST_CHANGES: + return changes_into((SzStream *)s->left, acc, remain, NULL, 0); + case SZ_ST_FLATMAP: + return flatmap_into((SzStream *)s->left, acc, remain, + (SzStreamMapFn)s->right, s->env); + case SZ_ST_FILTERNOT: + return filter_into((SzStream *)s->left, acc, remain, filter_not_pred, s); case SZ_ST_ZIP: case SZ_ST_ZIPWITH: case SZ_ST_ZIPALL: @@ -1342,12 +1364,387 @@ static SzIo *lift_into(SzStream *s, SzList *acc, int64_t remain) { int64_t n = (int64_t)(intptr_t)s->env; inner_remain = n <= 0 ? 0 : remain + n - 1; } - if ((s->tag == SZ_ST_TAKERIGHT || s->tag == SZ_ST_DROPRIGHT) && remain >= 0) + /* takeRight, dropRight, and findLast need the whole inner stream. */ + if ((s->tag == SZ_ST_TAKERIGHT || s->tag == SZ_ST_DROPRIGHT || + s->tag == SZ_ST_FINDLAST) && + remain >= 0) inner_remain = -1; return fm_drop(compile_into((SzStream *)s->left, sz_list_nil(), inner_remain), after_lift, st); } +static int64_t filter_not_pred(void *v, void *env) { + SzStream *node = (SzStream *)env; + SzStreamPred pred = (SzStreamPred)node->right; + return pred(v, node->env) == 0; +} + +typedef struct StMc { + SzStreamMapFn f; + void *fenv; + int64_t remain; + int64_t acc_len; + SzStream *next; + SzList *acc; +} StMc; + +static SzList *emit_chunk(SzList *acc, SzList *chunk, int64_t *remain) { + SzList *p = chunk; + while (!sz_list_is_empty(p) && *remain != 0) { + acc = cons_take(sz_list_head(p), acc); + p = sz_list_tail(p); + *remain = remain_dec(*remain); + } + return acc; +} + +static SzList *mc_apply(SzList *acc, void *v, SzStreamMapFn f, void *fenv, + int64_t *remain) { + SzList *chunk; + if (f) { + chunk = (SzList *)f(v, fenv); + acc = emit_chunk(acc, chunk, remain); + sz_release(chunk); + } else { + acc = emit_chunk(acc, (SzList *)v, remain); + } + return acc; +} + +static SzIo *after_mc_eval(void *value, void *env) { + StMc *st = (StMc *)env; + SzList *acc = st->acc; + SzStream *next = st->next; + int64_t remain = st->remain; + SzStreamMapFn f = st->f; + void *fenv = st->fenv; + sz_free(st); + acc = mc_apply(acc, value, f, fenv, &remain); + sz_release(value); + if (remain == 0) + return pure_drop(acc); + return mapconcat_into(next, acc, remain, f, fenv); +} + +static SzIo *after_mc_concat(void *acc, void *env) { + StMc *st = (StMc *)env; + SzStream *right = st->next; + int64_t remain = st->remain; + int64_t acc_len = st->acc_len; + SzStreamMapFn f = st->f; + void *fenv = st->fenv; + int64_t added; + sz_free(st); + if (remain >= 0) { + added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + remain = remain - added; + if (remain < 0) + remain = 0; + } + if (remain == 0) + return pure_drop(acc); + return mapconcat_into(right, (SzList *)acc, remain, f, fenv); +} + +static SzIo *mapconcat_into(SzStream *s, SzList *acc, int64_t remain, + SzStreamMapFn f, void *fenv) { + while (s && s->tag != SZ_ST_NIL) { + if (remain == 0) + return pure_drop(acc); + switch (s->tag) { + case SZ_ST_TAKE: { + int64_t n = (int64_t)(intptr_t)s->env; + if (n <= 0) + return pure_drop(acc); + if (remain < 0 || n < remain) + remain = n; + s = (SzStream *)s->left; + break; + } + case SZ_ST_CONS: + acc = mc_apply(acc, s->left, f, fenv, &remain); + s = (SzStream *)s->right; + break; + case SZ_ST_EVAL: { + StMc *st = (StMc *)sz_alloc(sizeof(StMc)); + st->f = f; + st->fenv = fenv; + st->remain = remain; + st->acc_len = 0; + st->next = (SzStream *)s->right; + st->acc = acc; + return fm_drop((SzIo *)s->left, after_mc_eval, st); + } + case SZ_ST_CONCAT: { + StMc *st = (StMc *)sz_alloc(sizeof(StMc)); + st->f = f; + st->fenv = fenv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->next = (SzStream *)s->right; + st->acc = acc; + return fm_drop(mapconcat_into((SzStream *)s->left, acc, remain, f, fenv), + after_mc_concat, st); + } + default: { + StLift *st = (StLift *)sz_alloc(sizeof(StLift)); + st->outer = acc; + st->remain = remain; + st->tag = f ? SZ_ST_MAPCONCAT : SZ_ST_FLATTEN; + st->arg = (void *)f; + st->env = fenv; + return fm_drop(compile_into(s, sz_list_nil(), -1), after_lift, st); + } + } + } + return pure_drop(acc); +} + +typedef struct StCh { + void *prev; + int have; + int64_t remain; + int64_t acc_len; + SzStream *next; + SzList *acc; +} StCh; + +static SzIo *after_ch_eval(void *value, void *env) { + StCh *st = (StCh *)env; + SzList *acc = st->acc; + SzStream *next = st->next; + int64_t remain = st->remain; + void *prev = st->prev; + int have = st->have; + sz_free(st); + if (!have || !sz_ptr_eq(prev, value)) { + acc = cons_take(value, acc); + prev = value; + have = 1; + remain = remain_dec(remain); + } + sz_release(value); + if (remain == 0) + return pure_drop(acc); + return changes_into(next, acc, remain, prev, have); +} + +static SzIo *after_ch_concat(void *acc, void *env) { + StCh *st = (StCh *)env; + SzStream *right = st->next; + int64_t remain = st->remain; + int64_t acc_len = st->acc_len; + void *prev = st->prev; + int have = st->have; + int64_t added; + sz_free(st); + added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + if (added > 0) { + prev = sz_list_head((SzList *)acc); + have = 1; + } + if (remain >= 0) { + remain = remain - added; + if (remain < 0) + remain = 0; + } + if (remain == 0) + return pure_drop(acc); + return changes_into(right, (SzList *)acc, remain, prev, have); +} + +static SzIo *changes_into(SzStream *s, SzList *acc, int64_t remain, void *prev, + int have) { + while (s && s->tag != SZ_ST_NIL) { + if (remain == 0) + return pure_drop(acc); + switch (s->tag) { + case SZ_ST_TAKE: { + int64_t n = (int64_t)(intptr_t)s->env; + if (n <= 0) + return pure_drop(acc); + if (remain < 0 || n < remain) + remain = n; + s = (SzStream *)s->left; + break; + } + case SZ_ST_CONS: + if (!have || !sz_ptr_eq(prev, s->left)) { + acc = cons_take(s->left, acc); + prev = s->left; + have = 1; + remain = remain_dec(remain); + } + s = (SzStream *)s->right; + break; + case SZ_ST_EVAL: { + StCh *st = (StCh *)sz_alloc(sizeof(StCh)); + st->prev = prev; + st->have = have; + st->remain = remain; + st->acc_len = 0; + st->next = (SzStream *)s->right; + st->acc = acc; + return fm_drop((SzIo *)s->left, after_ch_eval, st); + } + case SZ_ST_CONCAT: { + StCh *st = (StCh *)sz_alloc(sizeof(StCh)); + st->prev = prev; + st->have = have; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->next = (SzStream *)s->right; + st->acc = acc; + return fm_drop(changes_into((SzStream *)s->left, acc, remain, prev, have), + after_ch_concat, st); + } + default: { + StLift *st = (StLift *)sz_alloc(sizeof(StLift)); + st->outer = acc; + st->remain = remain; + st->tag = SZ_ST_CHANGES; + st->arg = NULL; + st->env = NULL; + return fm_drop(compile_into(s, sz_list_nil(), -1), after_lift, st); + } + } + } + return pure_drop(acc); +} + +typedef struct StFp { + SzStreamMapFn f; + void *fenv; + int64_t remain; + int64_t acc_len; + SzStream *next; + SzStream *cur; + SzList *acc; +} StFp; + +static SzIo *after_fp_inner(void *acc, void *env) { + StFp *st = (StFp *)env; + int64_t added = (int64_t)sz_list_len((SzList *)acc) - st->acc_len; + SzStream *next = st->next; + SzStreamMapFn f = st->f; + void *fenv = st->fenv; + int64_t remain = st->remain; + if (st->cur) { + sz_release(st->cur); + st->cur = NULL; + } + sz_free(st); + if (remain >= 0) { + remain -= added; + if (remain < 0) + remain = 0; + } + if (remain == 0) + return pure_drop(acc); + return flatmap_into(next, (SzList *)acc, remain, f, fenv); +} + +static SzIo *flatmap_one(void *v, SzList *acc, int64_t remain, SzStream *next, + SzStreamMapFn f, void *fenv, int own_v) { + StFp *st = (StFp *)sz_alloc(sizeof(StFp)); + SzStream *inner = (SzStream *)f(v, fenv); + if (own_v) + sz_release(v); + st->f = f; + st->fenv = fenv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->next = next; + st->cur = inner; + st->acc = acc; + return fm_drop(compile_into(inner, acc, remain), after_fp_inner, st); +} + +static SzIo *after_fp_eval(void *value, void *env) { + StFp *st = (StFp *)env; + SzList *acc = st->acc; + SzStream *next = st->next; + int64_t remain = st->remain; + SzStreamMapFn f = st->f; + void *fenv = st->fenv; + sz_free(st); + return flatmap_one(value, acc, remain, next, f, fenv, 1); +} + +static SzIo *after_fp_concat(void *acc, void *env) { + StFp *st = (StFp *)env; + SzStream *right = st->next; + int64_t remain = st->remain; + int64_t acc_len = st->acc_len; + SzStreamMapFn f = st->f; + void *fenv = st->fenv; + int64_t added; + sz_free(st); + if (remain >= 0) { + added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + remain = remain - added; + if (remain < 0) + remain = 0; + } + if (remain == 0) + return pure_drop(acc); + return flatmap_into(right, (SzList *)acc, remain, f, fenv); +} + +static SzIo *flatmap_into(SzStream *s, SzList *acc, int64_t remain, + SzStreamMapFn f, void *fenv) { + while (s && s->tag != SZ_ST_NIL) { + if (remain == 0) + return pure_drop(acc); + switch (s->tag) { + case SZ_ST_TAKE: { + int64_t n = (int64_t)(intptr_t)s->env; + if (n <= 0) + return pure_drop(acc); + if (remain < 0 || n < remain) + remain = n; + s = (SzStream *)s->left; + break; + } + case SZ_ST_CONS: + return flatmap_one(s->left, acc, remain, (SzStream *)s->right, f, fenv, 0); + case SZ_ST_EVAL: { + StFp *st = (StFp *)sz_alloc(sizeof(StFp)); + st->f = f; + st->fenv = fenv; + st->remain = remain; + st->acc_len = 0; + st->next = (SzStream *)s->right; + st->cur = NULL; + st->acc = acc; + return fm_drop((SzIo *)s->left, after_fp_eval, st); + } + case SZ_ST_CONCAT: { + StFp *st = (StFp *)sz_alloc(sizeof(StFp)); + st->f = f; + st->fenv = fenv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->next = (SzStream *)s->right; + st->cur = NULL; + st->acc = acc; + return fm_drop(flatmap_into((SzStream *)s->left, acc, remain, f, fenv), + after_fp_concat, st); + } + default: { + StLift *st = (StLift *)sz_alloc(sizeof(StLift)); + st->outer = acc; + st->remain = remain; + st->tag = SZ_ST_FLATMAP; + st->arg = (void *)f; + st->env = fenv; + return fm_drop(compile_into(s, sz_list_nil(), -1), after_lift, st); + } + } + } + return pure_drop(acc); +} + typedef struct StZip { SzStream *right; SzList *outer; @@ -1410,6 +1807,7 @@ static SzIo *zip_into(SzStream *s, SzList *acc, int64_t remain) { st->extra = s->env; { int64_t inner_remain = remain; + /* Interleave compiles both sides fully. take does not stop a side early. */ if (s->tag == SZ_ST_INTERLEAVE) inner_remain = -1; return fm_drop(compile_into((SzStream *)s->left, sz_list_nil(), inner_remain), @@ -1602,6 +2000,8 @@ static SzIo *reverse_acc(void *acc, void *env) { } static void *st_release_io(void *env) { + /* Delay env pins the stream (RC). The thunk stays empty. Last release + * is delay_env_drop on that env. */ (void)env; return NULL; } @@ -1648,21 +2048,16 @@ SzIo *sz_stream_exists(SzStream *s, SzStreamPred pred, void *env) { return fm_drop(io, exists_from_list, NULL); } -static SzIo *forall_from_list(void *list, void *env) { +static int64_t forall_miss_pred(void *v, void *env) { StFilter *st = (StFilter *)env; - SzList *xs = (SzList *)list; - int64_t ok = 1; - SzList *p = xs; - while (!sz_list_is_empty(p)) { - if (st->pred(sz_list_head(p), st->penv) == 0) { - ok = 0; - break; - } - p = sz_list_tail(p); - } - sz_release(xs); - sz_free(st); - return pure_drop(sz_box_i64(ok)); + return st->pred(v, st->penv) == 0; +} + +static SzIo *forall_from_exists(void *box, void *env) { + int64_t hit = sz_unbox_i64(box); + sz_release(box); + sz_free(env); + return pure_drop(sz_box_i64(hit ? 0 : 1)); } SzIo *sz_stream_forall(SzStream *s, SzStreamPred pred, void *env) { @@ -1671,7 +2066,8 @@ SzIo *sz_stream_forall(SzStream *s, SzStreamPred pred, void *env) { st->penv = env; st->remain = 0; st->acc_len = 0; - return fm_drop(sz_stream_compile_to_list(s), forall_from_list, st); + return fm_drop(sz_stream_exists(s, forall_miss_pred, st), forall_from_exists, + st); } static SzIo *fold_from_list(void *list, void *env) { @@ -1715,7 +2111,10 @@ static SzIo *head_from_list(void *list, void *env) { } SzIo *sz_stream_head(SzStream *s) { - return fm_drop(sz_stream_compile_to_list(s), head_from_list, NULL); + SzStream *one = sz_stream_take(s, 1); + SzIo *io = sz_stream_compile_to_list(one); + sz_release(one); + return fm_drop(io, head_from_list, NULL); } static SzIo *last_from_list(void *list, void *env) { diff --git a/crates/runtime/src/sys.c b/crates/runtime/src/sys.c index b7abe6d2..6d66bc75 100644 --- a/crates/runtime/src/sys.c +++ b/crates/runtime/src/sys.c @@ -346,6 +346,7 @@ SzIo *sz_sys_write(SzString *s) { } #define EXEC_CAP (1024 * 1024) +#define EXEC_FD_MAX 256 typedef struct ExecSt { SzString *cmd; @@ -353,6 +354,7 @@ typedef struct ExecSt { int err_fd; pid_t pid; int status; + int overflow; char *out_buf; size_t out_len; size_t out_cap; @@ -368,14 +370,32 @@ static void exec_close_fd(int *fd) { } } -static void exec_free(ExecSt *st) { +static void exec_close_extra_fds(void) { + int fd; + for (fd = 3; fd < EXEC_FD_MAX; fd++) + (void)close(fd); +} + +static void exec_reap_pid(pid_t pid) { int status = 0; + pid_t w; + if (pid <= 0) + return; + (void)kill(pid, SIGKILL); + do { + w = waitpid(pid, &status, 0); + } while (w < 0 && errno == EINTR); +} + +static void exec_free(ExecSt *st) { if (!st) return; exec_close_fd(&st->out_fd); exec_close_fd(&st->err_fd); - if (st->pid > 0) - (void)waitpid(st->pid, &status, WNOHANG); + if (st->pid > 0) { + exec_reap_pid(st->pid); + st->pid = 0; + } sz_free(st->out_buf); sz_free(st->err_buf); st->out_buf = NULL; @@ -384,6 +404,11 @@ static void exec_free(ExecSt *st) { st->cmd = NULL; } +static void *exec_cleanup(void *env) { + exec_free((ExecSt *)env); + return NULL; +} + static void exec_set_cloexec_nb(int fd) { int fl; (void)fcntl(fd, F_SETFD, FD_CLOEXEC); @@ -393,13 +418,14 @@ static void exec_set_cloexec_nb(int fd) { } static int exec_append(char **buf, size_t *len, size_t *cap, const char *src, - size_t n) { - size_t room; - if (*len >= EXEC_CAP) + size_t n, int *overflow) { + if (*len >= EXEC_CAP || n > EXEC_CAP - *len) { + if (overflow) { + *overflow = 1; + return 0; + } return 1; - room = EXEC_CAP - *len; - if (n > room) - n = room; + } if (!n) return 1; if (*len + n + 1 > *cap) { @@ -422,14 +448,16 @@ static int exec_append(char **buf, size_t *len, size_t *cap, const char *src, return 1; } -static int exec_drain_fd(int *fd, char **buf, size_t *len, size_t *cap) { +static int exec_drain_fd(int *fd, char **buf, size_t *len, size_t *cap, + int *overflow) { char tmp[4096]; if (!fd || *fd < 0) return 1; for (;;) { ssize_t n = read(*fd, tmp, sizeof tmp); if (n > 0) { - exec_append(buf, len, cap, tmp, (size_t)n); + if (!exec_append(buf, len, cap, tmp, (size_t)n, overflow)) + return 0; continue; } if (n == 0) { @@ -485,6 +513,7 @@ static void *sys_exec_start(void *env) { close(out_fds[1]); if (err_fds[1] != STDERR_FILENO && err_fds[1] != STDOUT_FILENO) close(err_fds[1]); + exec_close_extra_fds(); execl("/bin/sh", "sh", "-c", c, (char *)NULL); _exit(127); } @@ -535,9 +564,14 @@ static SzIo *exec_after_poll(void *value, void *env) { int status = 0; pid_t w = 0; (void)value; - if (!exec_drain_fd(&st->out_fd, &st->out_buf, &st->out_len, &st->out_cap) || - !exec_drain_fd(&st->err_fd, &st->err_buf, &st->err_len, &st->err_cap)) + if (!exec_drain_fd(&st->out_fd, &st->out_buf, &st->out_len, &st->out_cap, + &st->overflow) || + !exec_drain_fd(&st->err_fd, &st->err_buf, &st->err_len, &st->err_cap, + &st->overflow)) { + if (st->overflow) + return sz_io_fail_cstr("Sys.exec: output exceeds 1 MiB"); return sz_io_fail_cstr("Sys.exec: read failed"); + } if (st->pid > 0) { do { w = waitpid(st->pid, &status, WNOHANG); @@ -551,13 +585,10 @@ static SzIo *exec_after_poll(void *value, void *env) { if (st->out_fd >= 0 || st->err_fd >= 0) return exec_wait_ready(st); if (st->pid > 0) { - do { - w = waitpid(st->pid, &status, 0); - } while (w < 0 && errno == EINTR); + if (w == 0) + return fm_drop(sz_io_sleep_ms(1), exec_after_poll, st); if (w < 0) return sz_io_fail_cstr("Sys.exec: wait failed"); - st->status = status; - st->pid = 0; } return unwrap_sys(sys_exec_pack(st, exec_exit_code(st->status)), NULL); } @@ -576,31 +607,13 @@ static SzIo *exec_wait_ready(ExecSt *st) { return fm_drop(ready, exec_after_poll, st); } -static SzIo *exec_finish(void *code, void *env) { - exec_free((ExecSt *)env); - return pure_drop(code); -} - -static SzIo *exec_on_err(SzError *err, void *env) { - exec_free((ExecSt *)env); - return fail_drop(err); -} - static SzIo *exec_after_start(void *value, void *env) { ExecSt *st = (ExecSt *)env; SysResult *r = (SysResult *)value; - SzIo *io; if (!r || r->is_err) return unwrap_sys(value, NULL); sz_release(r); - io = exec_wait_ready(st); - return fm_drop(io, exec_finish, st); -} - -static SzIo *exec_keep_pair(void *value, void *env) { - (void)value; - (void)env; - return pure_drop(NULL); + return exec_wait_ready(st); } static SzIo *exec_after_kick(void *ignored, void *env) { @@ -615,10 +628,12 @@ static SzIo *exec_after_kick(void *ignored, void *env) { st->err_fd = -1; io = fm_drop(sz_io_delay(sys_exec_start, st), exec_after_start, st); { - SzIo *handled = sz_io_handle_error_with(io, exec_on_err, st); + SzIo *fin = sz_io_delay(exec_cleanup, st); + SzIo *ens = sz_io_ensure(io, fin); sz_release(io); + sz_release(fin); sz_release(st); - return handled; + return ens; } } @@ -630,8 +645,7 @@ static void *sys_proc_dispatch(void *env) { static SzIo *sys_after_exec_dispatch(void *value, void *env) { SzPair *p = (SzPair *)env; if ((intptr_t)value) - return fm_drop(sz_io_fail_cstr("Sys.exec: rejected under TestRuntime"), - exec_keep_pair, p); + return sz_io_fail_cstr("Sys.exec: rejected under TestRuntime"); return fm_drop(sz_io_pure(NULL), exec_after_kick, p); } @@ -705,7 +719,7 @@ static void child_clear(ChildSlot *c) { static int child_drain(ChildSlot *c) { if (!c) return 1; - return exec_drain_fd(&c->out_fd, &c->buf, &c->len, &c->cap); + return exec_drain_fd(&c->out_fd, &c->buf, &c->len, &c->cap, NULL); } static void child_gc(void) { @@ -721,9 +735,10 @@ static void child_gc(void) { } while (w < 0 && errno == EINTR); if (w == 0) continue; - if (!child_drain(c)) - continue; - if (c->len == 0 && c->out_fd < 0) + (void)child_drain(c); + exec_close_fd(&c->in_fd); + exec_close_fd(&c->out_fd); + if (c->len == 0) child_clear(c); } } @@ -787,6 +802,7 @@ static void *sys_spawn_result(void *env) { close(out_fds[0]); if (out_fds[1] != STDOUT_FILENO && out_fds[1] != STDIN_FILENO) close(out_fds[1]); + exec_close_extra_fds(); execl("/bin/sh", "sh", "-c", c, (char *)NULL); _exit(127); } @@ -808,8 +824,7 @@ static void *sys_spawn_result(void *env) { static SzIo *sys_after_spawn_dispatch(void *value, void *env) { SzPair *p = (SzPair *)env; if ((intptr_t)value) - return fm_drop(sz_io_fail_cstr("Sys.spawn: rejected under TestRuntime"), - exec_keep_pair, p); + return sz_io_fail_cstr("Sys.spawn: rejected under TestRuntime"); return fm_drop(sz_io_delay(sys_spawn_result, p), unwrap_sys, NULL); } @@ -867,7 +882,20 @@ static void *sys_kill_result(void *env) { r->as.err = sz_error_new(3, "Sys.kill: kill failed"); return r; } - child_drop_pid((pid_t)pid); + { + ChildSlot *c = child_find((pid_t)pid); + int status = 0; + pid_t w = 0; + if (c) + exec_close_fd(&c->in_fd); + if (pid > 0) { + do { + w = waitpid((pid_t)pid, &status, WNOHANG); + } while (w < 0 && errno == EINTR); + } + if (w > 0 || (w < 0 && errno == ECHILD) || pid <= 0) + child_drop_pid((pid_t)pid); + } return r; } @@ -1029,7 +1057,9 @@ static SzIo *child_read_after_try(void *value, void *env) { sz_release(r); child_gc(); c = child_find((pid_t)st->pid); - if (!c || c->out_fd < 0) + if (!c) + return sz_io_fail_cstr("Sys.childRead: unknown pid"); + if (c->out_fd < 0) return child_read_pump(NULL, st); return fm_drop(sz_io_poll_readable(c->out_fd), child_read_pump, st); } @@ -1043,8 +1073,8 @@ static void *child_read_try(void *env) { child_gc(); c = child_find((pid_t)st->pid); if (!c) { - r->is_err = 0; - r->as.ok = sz_string_from_cstr(""); + r->is_err = 1; + r->as.err = sz_error_new(3, "Sys.childRead: unknown pid"); return r; } if (!child_drain(c)) { diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index e2bee6ac..7d8eb308 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -59,31 +59,9 @@ static void sz_testrt_fs_install(void) { int sz_testrt_fs_is_fake(void) { return g_fs_fake; } -static char *norm_path(const char *p) { - size_t n; - char *out; - size_t i, j; - if (!p) - p = ""; - /* Strip leading ./ and collapse duplicate slashes; drop trailing slash. */ - while (p[0] == '.' && p[1] == '/') - p += 2; - n = strlen(p); - out = (char *)sz_alloc(n + 1); - j = 0; - for (i = 0; i < n; i++) { - if (p[i] == '/' && j > 0 && out[j - 1] == '/') - continue; - out[j++] = p[i]; - } - while (j > 0 && out[j - 1] == '/') - j--; - out[j] = '\0'; - /* Map a lone '.' to the root path. */ - if (strcmp(out, ".") == 0) - out[0] = '\0'; - return out; -} +static char *canon_path(const char *p); + +static char *norm_path(const char *p) { return canon_path(p); } static MemNode *fs_find(const char *path) { MemNode *n; @@ -391,10 +369,10 @@ void sz_effect_log(const char *line) { sz_timeline_log_cstr(line ? line : "", ""); } -static void fs_log(const char *op) { +static void fs_log(const char *op, const char *path) { char buf[48]; snprintf(buf, sizeof buf, "Fs.%s", op); - sz_timeline_log_cstr(buf, ""); + sz_timeline_log_cstr(buf, path ? path : ""); } static int fs_fault(BoxResult *r) { @@ -422,6 +400,11 @@ static void *mem_read(void *env) { if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.read: path too deep (mem)"); + goto done; + } n = fs_find(path); sz_free(path); if (!n || n->is_dir) { @@ -447,12 +430,16 @@ static void *mem_write(void *env) { char *path; char parent[1024]; MemNode *n; - SzString *c; - sz_timeline_log_bytes("Fs.write", contents ? contents->data : "", - contents ? contents->len : 0); + SzString *c = contents ? contents : NULL; + sz_timeline_log_cstr("Fs.write", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.write: path too deep (mem)"); + goto done; + } c = contents ? contents : sz_string_from_cstr(""); if (!parent_path(path, parent, sizeof parent)) { @@ -539,10 +526,15 @@ static void *mem_list(void *env) { MemNode *dir; MemNode *n; SzList *acc; - fs_log("list"); + fs_log("list", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.list: path too deep (mem)"); + goto done; + } dir = fs_find(path); if (!dir || !dir->is_dir) { @@ -592,14 +584,20 @@ static void *mem_exists(void *env) { BoxResult *r = (BoxResult *)rc_box_zero(sizeof(BoxResult)); char *path; MemNode *n; - fs_log("exists"); + fs_log("exists", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.exists: path too deep (mem)"); + goto done; + } n = fs_find(path); sz_free(path); r->is_err = 0; r->as.ok = sz_box_i64(n ? 1 : 0); +done: return r; } @@ -636,11 +634,16 @@ static void *mem_delete(void *env) { char *path; MemNode *n; MemNode **pp; - fs_log("delete"); + fs_log("delete", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); - if (!path[0]) { + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.delete: path too deep (mem)"); + goto done; + } + if (!path[0] || (path[0] == '/' && path[1] == '\0')) { sz_free(path); r->is_err = 1; r->as.err = sz_error_new(2, "Fs.delete: refused root (mem)"); @@ -685,12 +688,20 @@ static void *mem_rename(void *env) { MemNode *src; MemNode *n; size_t from_len; - fs_log("rename"); + fs_log("rename", from_s ? sz_string_cstr(from_s) : ""); if (fs_fault(r)) return r; from = norm_path(sz_string_cstr(from_s)); to = norm_path(sz_string_cstr(to_s)); - if (!from[0] || !to[0]) { + if (!from || !to) { + sz_free(from); + sz_free(to); + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.rename: path too deep (mem)"); + goto done; + } + if (!from[0] || !to[0] || (from[0] == '/' && from[1] == '\0') || + (to[0] == '/' && to[1] == '\0')) { sz_free(from); sz_free(to); r->is_err = 1; @@ -773,10 +784,15 @@ static void *mem_walk(void *env) { MemNode *n; SzList *acc; size_t dlen; - fs_log("walk"); + fs_log("walk", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.walk: path too deep (mem)"); + goto done; + } dir = fs_find(path); if (!dir || !dir->is_dir) { sz_free(path); @@ -828,10 +844,15 @@ static void *mem_mkdirs(void *env) { char tmp[1024]; size_t len; size_t i; - fs_log("mkdirs"); + fs_log("mkdirs", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = norm_path(sz_string_cstr(path_s)); + if (!path) { + r->is_err = 1; + r->as.err = sz_error_new(2, "Fs.mkdirs: path too deep (mem)"); + goto done; + } len = strlen(path); if (len == 0) { @@ -877,53 +898,50 @@ SzIo *sz_testrt_fs_mkdirs(SzString *path) { } static char *canon_path(const char *p) { - char *norm = norm_path(p); char *parts[256]; size_t nparts = 0; size_t i = 0; int abs = 0; char *out; size_t out_len = 1; - if (norm[0] == '/') + if (!p) + p = ""; + if (p[0] == '/') abs = 1; - while (norm[i]) { + while (p[i]) { size_t start = i; - while (norm[i] && norm[i] != '/') + while (p[i] && p[i] != '/') i++; if (i > start) { size_t n = i - start; - char *seg = (char *)sz_alloc(n + 1); - memcpy(seg, norm + start, n); - seg[n] = '\0'; - if (strcmp(seg, ".") == 0) { - sz_free(seg); - } else if (strcmp(seg, "..") == 0) { - sz_free(seg); + if (n == 1 && p[start] == '.') { + /* skip "." */ + } else if (n == 2 && p[start] == '.' && p[start + 1] == '.') { + /* .. past the virtual root stays at root. */ if (nparts > 0) { sz_free(parts[nparts - 1]); nparts--; } } else if (nparts < 256) { + char *seg = (char *)sz_alloc(n + 1); + memcpy(seg, p + start, n); + seg[n] = '\0'; parts[nparts++] = seg; } else { - /* Fail when the path has too many segments. Do not drop extra segments. */ size_t k; - sz_free(seg); - sz_free(norm); for (k = 0; k < nparts; k++) sz_free(parts[k]); return NULL; } } - if (norm[i] == '/') + if (p[i] == '/') i++; } - sz_free(norm); for (i = 0; i < nparts; i++) out_len += strlen(parts[i]) + 1; out = (char *)sz_alloc(out_len + 2); out[0] = '\0'; - if (abs) + if (abs && nparts > 0) strcat(out, "/"); for (i = 0; i < nparts; i++) { if (i > 0) @@ -931,10 +949,6 @@ static char *canon_path(const char *p) { strcat(out, parts[i]); sz_free(parts[i]); } - if (abs && nparts == 0) { - out[0] = '/'; - out[1] = '\0'; - } return out; } @@ -944,7 +958,7 @@ static void *mem_canonicalize(void *env) { BoxResult *r = (BoxResult *)rc_box_zero(sizeof(BoxResult)); char *path; MemNode *n; - fs_log("canonicalize"); + fs_log("canonicalize", path_s ? sz_string_cstr(path_s) : ""); if (fs_fault(r)) return r; path = canon_path(sz_string_cstr(path_s)); @@ -1538,6 +1552,7 @@ void sz_testrt_net_set_last_serve_body(const char *body) { enum { FAKE_CAP = 64 }; enum { FK_TCP = 1, FK_LISTEN = 2, FK_UDP = 3 }; +enum { FAKE_READ_MAX = 1024u * 1024u }; typedef struct FakeDgram { char *host; @@ -1558,10 +1573,12 @@ typedef struct FakeSock { size_t len; size_t cap; SzDeferred *read_wait; + size_t read_want; SzDeferred *accept_wait; int accept_q[16]; int accept_n; FakeDgram *dgrams; + FakeDgram *dgrams_tail; SzDeferred *recv_wait; } FakeSock; @@ -1762,9 +1779,9 @@ static SzNetSock *fake_take_accept(FakeSock *ln) { return fake_wrap(srv); } -static void fake_deliver_accept(FakeSock *ln, FakeSock *server) { +static int fake_deliver_accept(FakeSock *ln, FakeSock *server) { if (!ln || !server) - return; + return 0; if (ln->accept_wait) { SzNetSock *wrap = fake_wrap(server); SzDeferred *w = ln->accept_wait; @@ -1772,11 +1789,12 @@ static void fake_deliver_accept(FakeSock *ln, FakeSock *server) { sz_deferred_complete_now(w, wrap); sz_release(w); sz_release(wrap); - return; + return 1; } if (ln->accept_n >= 16) - return; + return 0; ln->accept_q[ln->accept_n++] = server->id; + return 1; } static void fake_flush_connects(FakeSock *ln) { @@ -1790,20 +1808,35 @@ static void fake_flush_connects(FakeSock *ln) { *pp = p->next; { FakeSock *cli = fake_alloc(FK_TCP, ln->port); - FakeSock *srv = fake_alloc(FK_TCP, ln->port); + FakeSock *srv; SzNetSock *wrap; - if (!cli || !srv) { + if (!cli) { + sz_deferred_fail_now(p->wait, sz_error_new(6, "Net.tcpConnect: full")); + sz_release(p->wait); + sz_free(p); + continue; + } + srv = fake_alloc(FK_TCP, ln->port); + if (!srv) { + fake_slot_clear(cli); sz_deferred_fail_now(p->wait, sz_error_new(6, "Net.tcpConnect: full")); sz_release(p->wait); sz_free(p); continue; } fake_pair(cli, srv); + if (!fake_deliver_accept(ln, srv)) { + fake_slot_clear(cli); + fake_slot_clear(srv); + sz_deferred_fail_now(p->wait, sz_error_new(6, "Net.tcpConnect: connect failed")); + sz_release(p->wait); + sz_free(p); + continue; + } wrap = fake_wrap(cli); sz_deferred_complete_now(p->wait, wrap); sz_release(p->wait); sz_release(wrap); - fake_deliver_accept(ln, srv); } sz_free(p); } @@ -1830,11 +1863,19 @@ static BoxResult *fake_connect_pair(int64_t port) { if (!ln) return NULL; cli = fake_alloc(FK_TCP, port); + if (!cli) + return box_err("Net.tcpConnect: connect failed"); srv = fake_alloc(FK_TCP, port); - if (!cli || !srv) + if (!srv) { + fake_slot_clear(cli); return box_err("Net.tcpConnect: connect failed"); + } fake_pair(cli, srv); - fake_deliver_accept(ln, srv); + if (!fake_deliver_accept(ln, srv)) { + fake_slot_clear(cli); + fake_slot_clear(srv); + return box_err("Net.tcpConnect: connect failed"); + } return box_ok(fake_wrap(cli)); } @@ -1979,6 +2020,8 @@ static void *tcp_read_now(void *env) { if (!f || f->closed) return box_err("Net.tcpRead: closed"); want = op->n > 0 ? (size_t)op->n : 0; + if (want > FAKE_READ_MAX) + want = FAKE_READ_MAX; if (want == 0) return box_ok(sz_string_from_cstr("")); if (f->len > 0) @@ -1997,13 +2040,17 @@ static SzIo *after_tcp_read(void *value, void *env) { if (!f || f->closed) return sz_io_fail_cstr("Net.tcpRead: closed"); want = op->n > 0 ? (size_t)op->n : 0; + if (want > FAKE_READ_MAX) + want = FAKE_READ_MAX; if (f->len > 0) return unwrap_box(box_ok(fake_take(f, want)), NULL); d = sz_deferred_make(); f->read_wait = d; + f->read_want = want; if (f->len > 0) { SzString *got = fake_take(f, want); f->read_wait = NULL; + f->read_want = 0; sz_release(d); return pure_drop(got); } @@ -2044,8 +2091,9 @@ static void *tcp_write_now(void *env) { if (peer->read_wait) { SzDeferred *w = peer->read_wait; SzString *got; - size_t want = n; + size_t want = peer->read_want; peer->read_wait = NULL; + peer->read_want = 0; got = fake_take(peer, want); sz_deferred_complete_now(w, got); sz_release(w); @@ -2122,17 +2170,46 @@ typedef struct UdpSendPack { int64_t port; } UdpSendPack; +static void fake_dgram_push(FakeSock *dst, FakeDgram *d) { + d->next = NULL; + if (dst->dgrams_tail) + dst->dgrams_tail->next = d; + else + dst->dgrams = d; + dst->dgrams_tail = d; +} + +static FakeDgram *fake_dgram_pop(FakeSock *f) { + FakeDgram *d = f->dgrams; + if (!d) + return NULL; + f->dgrams = d->next; + if (!f->dgrams) + f->dgrams_tail = NULL; + d->next = NULL; + return d; +} + static void *udp_send_now(void *env) { UdpSendPack *p = (UdpSendPack *)env; FakeSock *src = p && p->sock ? fake_by_id(p->sock->fake_id) : NULL; FakeSock *dst; FakeDgram *d; const char *data; + const char *h; size_t n; + char canon[64]; + int fam; if (!src || src->kind != FK_UDP || src->closed) return box_err("Net.udpSend: closed"); if (p->port < 1 || p->port > 65535) return box_err("Net.udpSend: host must be an IP literal"); + h = p->host ? sz_string_cstr(p->host) : ""; + fam = sz_net_host_family(h, canon, sizeof canon); + if (!fam) + return box_err("Net.udpSend: host must be an IP literal"); + if (fam == 6) + return box_err("Net.udpSend: send failed"); dst = fake_udp_port(p->port); if (!dst) return box_err("Net.udpSend: send failed"); @@ -2140,7 +2217,7 @@ static void *udp_send_now(void *env) { n = p->data ? (size_t)sz_string_len(p->data) : 0; d = (FakeDgram *)sz_alloc(sizeof(FakeDgram)); memset(d, 0, sizeof(FakeDgram)); - d->host = dup_cstr("127.0.0.1"); + d->host = dup_cstr(canon[0] ? canon : "127.0.0.1"); d->port = src->port; d->data = (char *)sz_alloc(n + 1); if (n) @@ -2164,8 +2241,7 @@ static void *udp_send_now(void *env) { sz_release(outer); fake_dgram_free(d); } else { - d->next = dst->dgrams; - dst->dgrams = d; + fake_dgram_push(dst, d); } return box_ok(NULL); } @@ -2211,8 +2287,7 @@ static void *udp_recv_now(void *env) { return box_err("Net.udpRecv: closed"); if (!f->dgrams) return NULL; - d = f->dgrams; - f->dgrams = d->next; + d = fake_dgram_pop(f); n = d->len; if (p->n > 0 && (size_t)p->n < n) n = (size_t)p->n; @@ -2393,12 +2468,6 @@ void sz_testrt_proc_kill(int64_t pid) { } } -static void env_copy_host(const char *key) { - const char *v = getenv(key); - if (v && v[0]) - sz_testrt_env_set(key, v); -} - static void free_fake_argv(void) { int i; if (!g_fake_argv) @@ -2557,13 +2626,23 @@ SzIo *sz_testrt_sys_read(int64_t n) { return io; } +/* Drive.testEnv sets these on the process. Copy them into the sealed + * map so Sys.getenv can arm kit gates without reading the host map. */ +static void env_seed_drive(void) { + static const char *const keys[] = {"SCUZZ_SERVE", "SCUZZ_KIT"}; + size_t i; + const char *v; + for (i = 0; i < sizeof keys / sizeof keys[0]; i++) { + v = getenv(keys[i]); + if (v && v[0]) + sz_testrt_env_set(keys[i], v); + } +} + static void sz_testrt_sys_install(void) { sz_testrt_sys_reset_live(); g_sys_fake = 1; - /* App fixtures the CLI sets. Do not copy PATH/HOME/SCUZZ_TESTRT. */ - env_copy_host("SCUZZ_TODO_PATH"); - env_copy_host("SCUZZ_SERVE"); - env_copy_host("SCUZZ_KIT"); + env_seed_drive(); } int sz_testrt_sys_is_fake(void) { return g_sys_fake; } @@ -2571,9 +2650,17 @@ int sz_testrt_sys_is_fake(void) { return g_sys_fake; } /* --- install / reset ----------------------------------------------------- */ void sz_testrt_install(void) { + uint64_t rand_seed = 42; + const char *rs; fault_arm_from_env(); sz_testrt_clock_install(1); - sz_testrt_random_install(42); + rs = getenv("SCUZZ_RAND_SEED"); + if (rs && rs[0]) { + unsigned long long parsed = strtoull(rs, NULL, 10); + if (parsed != 0) + rand_seed = (uint64_t)parsed; + } + sz_testrt_random_install(rand_seed); sz_testrt_fs_install(); sz_testrt_net_install(); sz_testrt_sys_install(); @@ -3138,11 +3225,93 @@ int64_t sz_timeline_replay_signal_int(const char *name) { return sep ? (int64_t)atoll(sep + 3) : 0; } +static const char *tl_sig_payload(const char *sep) { + if (!sep || memcmp(sep, " = ", 3) != 0) + return NULL; + return sep + 3; +} + +static SzString *tl_parse_quoted_str(const char *sep) { + const char *p = tl_sig_payload(sep); + char *val = NULL; + SzString *out; + if (!p || *p != '"') + return sz_string_from_cstr(""); + if (!sz_dump_parse_quoted(p, &val) || !val) + return sz_string_from_cstr(""); + out = sz_string_from_cstr(val); + sz_free(val); + return out; +} + +static int64_t tl_count_quoted_list(const char *p) { + int64_t n = 0; + if (!p) + return 0; + while (*p && *p != ']' && *p != '\n') { + while (*p == ' ' || *p == ',') + p++; + if (*p != '"') + break; + p = sz_dump_parse_quoted(p, NULL); + if (!p) + break; + n++; + } + return n; +} + static int64_t tl_parse_signal_int(const char *dump, const char *name) { const char *sep = tl_sig_line(dump, "int", name); return sep ? (int64_t)atoll(sep + 3) : 0; } +SzString *sz_timeline_replay_signal_str(const char *name) { + return tl_parse_quoted_str(tl_sig_line(g_replay_signals, "str", name)); +} + +int64_t sz_timeline_replay_signal_list_len(const char *name) { + const char *sep = tl_sig_line(g_replay_signals, "list", name); + const char *p; + if (!sep) + return 0; + if (memcmp(sep, " = <", 4) == 0) + return (int64_t)atoll(sep + 4); + if (memcmp(sep, " = [", 4) != 0) + return 0; + p = sep + 4; + return tl_count_quoted_list(p); +} + +SzString *sz_timeline_replay_signal_list_at(const char *name, int64_t index) { + const char *sep = tl_sig_line(g_replay_signals, "list", name); + const char *p; + int64_t i = 0; + if (index < 0 || !sep || memcmp(sep, " = [", 4) != 0) + return sz_string_from_cstr(""); + p = sep + 4; + while (*p && *p != ']' && *p != '\n') { + char *val = NULL; + while (*p == ' ' || *p == ',') + p++; + if (*p != '"') + break; + p = sz_dump_parse_quoted(p, &val); + if (!p) { + sz_free(val); + break; + } + if (i == index) { + SzString *out = sz_string_from_cstr(val ? val : ""); + sz_free(val); + return out; + } + sz_free(val); + i++; + } + return sz_string_from_cstr(""); +} + static SzTlState *tl_at(void *tl, int64_t i) { SzTimeline *t = (SzTimeline *)tl; if (!t || i < 0 || i >= t->n) @@ -3161,12 +3330,11 @@ int64_t sz_timeline_signal_int(void *tl, int64_t i, SzString *name) { : 0; } -/* List length from the signals dump: `list[] = ["a", "b"]` holds - * one quoted string per element (no escaping), so quotes pair per element. */ +/* List length from the signals dump: `list[] = ["a", "b"]` + * counts quoted strings with the dump escape dialect. */ static int64_t tl_parse_signal_list_len(const char *dump, const char *name) { const char *sep = tl_sig_line(dump, "list", name); const char *p; - int64_t quotes = 0; if (!sep) return 0; /* Record lists dump the count only: `list[] = `. */ @@ -3175,12 +3343,7 @@ static int64_t tl_parse_signal_list_len(const char *dump, const char *name) { if (memcmp(sep, " = [", 4) != 0) return 0; p = sep + 4; - while (*p && *p != ']' && *p != '\n') { - if (*p == '"') - quotes += 1; - p += 1; - } - return quotes / 2; + return tl_count_quoted_list(p); } int64_t sz_timeline_signal_list_len(void *tl, int64_t i, SzString *name) { @@ -3190,35 +3353,27 @@ int64_t sz_timeline_signal_list_len(void *tl, int64_t i, SzString *name) { : 0; } -/* 1 when the `str[] = ""` line in the state's signals dump - * holds `needle` as a substring, searched from the ` = ` separator so the - * name cannot false-match. */ +/* 1 when the unescaped `str[] = ""` holds `needle`. + * An empty needle is true when the value is empty. */ int64_t sz_timeline_signal_str_has(void *tl, int64_t i, SzString *name, SzString *needle) { - const char *p; - const char *end; SzTlState *s = tl_at(tl, i); const char *n = needle ? sz_string_cstr(needle) : ""; - if (!s || !s->signals || !n[0]) + const char *sep; + const char *p; + char *val = NULL; + int64_t hit = 0; + if (!s || !s->signals) return 0; - p = tl_sig_line(s->signals, "str", name ? sz_string_cstr(name) : ""); - if (!p) + sep = tl_sig_line(s->signals, "str", name ? sz_string_cstr(name) : ""); + p = tl_sig_payload(sep); + if (!p || *p != '"') return 0; - end = strchr(p, '\n'); - if (!end) - end = p + strlen(p); - { - size_t len = (size_t)(end - p); - size_t m = strlen(n); - size_t k; - if (m > len) - return 0; - for (k = 0; k + m <= len; k++) { - if (memcmp(p + k, n, m) == 0) - return 1; - } - } - return 0; + if (!sz_dump_parse_quoted(p, &val) || !val) + return 0; + hit = !n[0] ? (val[0] == '\0' ? 1 : 0) : (strstr(val, n) != NULL ? 1 : 0); + sz_free(val); + return hit; } int64_t sz_timeline_a11y_has(void *tl, int64_t i, SzString *needle) { diff --git a/crates/runtime/src/ui.c b/crates/runtime/src/ui.c index 15aa60ea..4392c4e1 100644 --- a/crates/runtime/src/ui.c +++ b/crates/runtime/src/ui.c @@ -13,6 +13,8 @@ #include #include #include +#include +#include static int want_gpu_presenter(void) { const char *e = getenv("SCUZZ_SKIA"); @@ -170,6 +172,9 @@ struct SzUiSession { static SzUiSession *g_live_session; static char *g_pending_title; +static void host_free(char **p); +static void session_drop_pointer(SzUiSession *session); + static int runtime_kind_ok(SzUiRuntimeKind kind) { return kind == SZ_UI_RUNTIME_HEADLESS || kind == SZ_UI_RUNTIME_DESKTOP || kind == SZ_UI_RUNTIME_MOBILE; @@ -265,6 +270,7 @@ void sz_ui_session_take_root(SzUiSession *session) { int sz_ui_session_replace_root(SzUiSession *session, SzView *root) { if (!session || !root) return 0; + session_drop_pointer(session); if (session->owns_view) sz_view_free(session->root); session->root = root; @@ -273,21 +279,65 @@ int sz_ui_session_replace_root(SzUiSession *session, SzView *root) { return 1; } -enum { SZ_UI_STAMP_CAP = 4096 }; - -static char *stamp_snapshot(const char *path) { +/* Whole-file read for inject playback (prefix-extend). Grows like signal dump. */ +static char *read_file_all(const char *path) { FILE *f; - char buf[SZ_UI_STAMP_CAP]; + char *buf = NULL; + size_t len = 0, cap = 0; + char tmp[4096]; size_t n; if (!path) return sz_strdup(""); f = fopen(path, "rb"); if (!f) return sz_strdup(""); - n = fread(buf, 1, sizeof(buf) - 1, f); + while ((n = fread(tmp, 1, sizeof tmp, f)) > 0) { + if (len + n + 1 > cap) { + size_t ncap = cap ? cap : 256; + char *nb; + while (len + n + 1 > ncap) + ncap *= 2; + nb = (char *)sz_alloc(ncap); + if (buf) { + memcpy(nb, buf, len); + sz_free(buf); + } + buf = nb; + cap = ncap; + } + memcpy(buf + len, tmp, n); + len += n; + } + fclose(f); + if (!buf) + return sz_strdup(""); + buf[len] = '\0'; + return buf; +} + +/* Small watch stamp: length plus FNV-1a of the whole file. */ +static char *watch_stamp(const char *path) { + FILE *f; + unsigned char tmp[4096]; + size_t n, total = 0; + uint32_t h = 2166136261u; + char out[64]; + if (!path) + return sz_strdup("0:0"); + f = fopen(path, "rb"); + if (!f) + return sz_strdup("0:0"); + while ((n = fread(tmp, 1, sizeof tmp, f)) > 0) { + size_t i; + total += n; + for (i = 0; i < n; i++) { + h ^= tmp[i]; + h *= 16777619u; + } + } fclose(f); - buf[n] = '\0'; - return sz_strdup(buf); + snprintf(out, sizeof out, "%zu:%08x", total, h); + return sz_strdup(out); } static int stamp_changed(SzUiSession *session) { @@ -295,7 +345,7 @@ static int stamp_changed(SzUiSession *session) { int changed; if (!session || !session->watch_path) return 0; - now = stamp_snapshot(session->watch_path); + now = watch_stamp(session->watch_path); changed = !session->watch_fp || strcmp(session->watch_fp, now) != 0; if (changed) { sz_free(session->watch_fp); @@ -322,7 +372,7 @@ int sz_ui_session_watch(SzUiSession *session, const char *path) { sz_free(session->watch_path); sz_free(session->watch_fp); session->watch_path = sz_strdup(path); - session->watch_fp = stamp_snapshot(path); + session->watch_fp = watch_stamp(path); return 1; } @@ -344,7 +394,7 @@ int sz_ui_session_set_inject(SzUiSession *session, const char *path) { sz_free(session->inject_path); sz_free(session->inject_fp); session->inject_path = sz_strdup(path); - session->inject_fp = stamp_snapshot(path); + session->inject_fp = read_file_all(path); return 1; } @@ -381,26 +431,30 @@ static void fputs_dump_quoted(FILE *f, const char *s) { } /* Editor dump: keep newlines as \\n so a file buffer stays one node. */ -static void fputs_dump_escaped(FILE *f, const char *s) { +static void fputs_escaped_body(FILE *f, const char *s) { const char *p; - fputc('"', f); - if (s) { - for (p = s; *p; p++) { - unsigned char c = (unsigned char)*p; - if (c == '\\') - fputs("\\\\", f); - else if (c == '"') - fputs("\\\"", f); - else if (c == '\n') - fputs("\\n", f); - else if (c == '\r') - fputs("\\r", f); - else if (c == '\t') - fputs("\\t", f); - else - fputc(*p, f); - } + if (!s) + return; + for (p = s; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c == '\\') + fputs("\\\\", f); + else if (c == '"') + fputs("\\\"", f); + else if (c == '\n') + fputs("\\n", f); + else if (c == '\r') + fputs("\\r", f); + else if (c == '\t') + fputs("\\t", f); + else + fputc(*p, f); } +} + +static void fputs_dump_escaped(FILE *f, const char *s) { + fputc('"', f); + fputs_escaped_body(f, s); fputc('"', f); } @@ -671,16 +725,22 @@ int sz_ui_session_load_code(SzUiSession *session, const char *path) { if (snprintf(staged, sizeof staged, "%s.load-%d", path, session->code_gen) >= (int)sizeof staged) return 0; - if (!copy_file(path, staged)) + if (!copy_file(path, staged)) { + unlink(staged); return 0; + } h = dlopen(staged, RTLD_NOW | RTLD_LOCAL); - if (!h) + if (!h) { + unlink(staged); return 0; + } fn = (SzUiRebuildFn)dlsym(h, "sz_ui_reload_rebuild"); if (!fn) { dlclose(h); + unlink(staged); return 0; } + unlink(staged); session->code_stale = session->code_handle; session->code_handle = h; session->rebuild = fn; @@ -749,11 +809,31 @@ static void host_free(char **p) { *p = NULL; } +static void session_drop_pointer(SzUiSession *session) { + if (!session) + return; + session->pointer_down = 0; + session->pointer_button = 0; + session->pointer_scroll = NULL; + session->pointer_slider = NULL; + session->pointer_field = NULL; + session->hover_seen = 0; + host_free(&session->hover_desc); + session->last_hit_seen = 0; + host_free(&session->last_hit_desc); + session->last_secondary_seen = 0; + host_free(&session->last_secondary_desc); + host_free(&session->record_hover_desc); + if (session->root) + sz_view_clear_hover(session->root); +} + void sz_ui_unmount(SzUiSession *session) { if (!session) return; if (g_live_session == session) g_live_session = NULL; + session_drop_pointer(session); sz_ui_bridge_flush(session); pthread_mutex_destroy(&session->bridge_lock); if (session->cfg.kind == SZ_UI_RUNTIME_DESKTOP) @@ -1128,9 +1208,11 @@ static void record_clipboard_verb(SzUiSession *session, int op) { fputs("copy\n", f); else if (op == 2) fputs("cut\n", f); - else if (session->clipboard && session->clipboard[0]) - fprintf(f, "paste %s\n", session->clipboard); - else + else if (session->clipboard && session->clipboard[0]) { + fputs("paste ", f); + fputs_escaped_body(f, session->clipboard); + fputc('\n', f); + } else fputs("paste\n", f); fclose(f); } @@ -1171,15 +1253,20 @@ static void record_live_event(SzUiSession *session, const SzInputEvent *ev) { if (!clipboard_chord(ev->key, ev->key_mods)) record_key_line(f, ev); } else if (ev->kind == SZ_INPUT_COMPOSE) { - if (ev->text && ev->text[0]) - fprintf(f, "compose %s\n", ev->text); - else + if (ev->text && ev->text[0]) { + fputs("compose ", f); + fputs_escaped_body(f, ev->text); + fputc('\n', f); + } else fputs("commit\n", f); } else if (ev->kind == SZ_INPUT_TEXT_EDIT) { if (!ev->text || !ev->text[0]) fputs("backspace\n", f); - else - fprintf(f, "type %s\n", ev->text); + else { + fputs("type ", f); + fputs_escaped_body(f, ev->text); + fputc('\n', f); + } } else if (ev->kind == SZ_INPUT_POINTER && ev->pointer_phase == SZ_POINTER_MOVE && !session->pointer_down) { SzView *tip; @@ -1279,7 +1366,7 @@ static int take_inject(SzUiSession *session, char **out) { if (!session || !session->inject_path || !out) return 0; *out = NULL; - now = stamp_snapshot(session->inject_path); + now = read_file_all(session->inject_path); if (session->inject_fp && strcmp(session->inject_fp, now) == 0) { sz_free(now); return 0; diff --git a/crates/runtime/src/ui_script.c b/crates/runtime/src/ui_script.c index 0afcce86..939e079f 100644 --- a/crates/runtime/src/ui_script.c +++ b/crates/runtime/src/ui_script.c @@ -1,6 +1,7 @@ #include "ui_script.h" #include "scuzz_rt.h" +#include "rt_util.h" #include #include @@ -506,7 +507,9 @@ static void play_script_line(SzUiSession *session, char *line) { } else if (strncmp(line, "type ", 5) == 0 || strcmp(line, "type") == 0) { int idx; const char *payload = script_field_payload(len > 4 ? line + 5 : "", &idx); - script_type(session, idx, payload); + char *raw = sz_dump_unescape(payload); + script_type(session, idx, raw); + sz_free(raw); } else if (strncmp(line, "key ", 4) == 0 || strcmp(line, "key") == 0) { const char *rest = len > 3 ? line + 4 : ""; char token[96]; @@ -528,9 +531,12 @@ static void play_script_line(SzUiSession *session, char *line) { script_key(session, name, text, mods, repeat); } else if (strncmp(line, "compose ", 8) == 0 || strcmp(line, "compose") == 0) { const char *rest = len > 7 ? line + 8 : ""; + char *raw; while (*rest == ' ') rest++; - script_compose(session, rest); + raw = sz_dump_unescape(rest); + script_compose(session, raw); + sz_free(raw); } else if (strcmp(line, "commit") == 0) { script_compose(session, ""); } else if (strncmp(line, "caret ", 6) == 0 || strcmp(line, "caret") == 0) { @@ -559,10 +565,16 @@ static void play_script_line(SzUiSession *session, char *line) { fprintf(stderr, "scuzz: script cut skipped (no text field)\n"); } else if (strncmp(line, "paste ", 6) == 0 || strcmp(line, "paste") == 0) { const char *payload = NULL; + char *raw = NULL; if (len > 6 && line[5] == ' ' && line[6]) payload = line + 6; + if (payload) { + raw = sz_dump_unescape(payload); + payload = raw; + } if (!sz_ui_session_paste(session, payload)) fprintf(stderr, "scuzz: script paste skipped (no text field)\n"); + sz_free(raw); } else if (strncmp(line, "drag ", 5) == 0) { float x1 = 0.f, y1 = 0.f, x2 = 0.f, y2 = 0.f; if (sscanf(line + 5, "%f %f %f %f", &x1, &y1, &x2, &y2) == 4) diff --git a/crates/runtime/src/view.c b/crates/runtime/src/view.c index f7060d19..9cdaa8bc 100644 --- a/crates/runtime/src/view.c +++ b/crates/runtime/src/view.c @@ -55,7 +55,7 @@ struct SzView { /* View.each: rebuild children from Signal.list at layout (pull). */ SzSignalList *each_sig; - SzList *each_seen; /* last synced list pointer (not owned) */ + SzList *each_seen; /* last synced list (retained; sentinel 1 = never synced) */ SzViewEachFn each_fn; void *each_env; @@ -1090,9 +1090,9 @@ static const char *a11y_role_name(SzA11yRole role) { } } -static void a11y_dump_node(SzView *v, char *buf, size_t cap, size_t *len) { +static void a11y_dump_node(SzView *v, char **buf, size_t *len, size_t *cap) { int i; - if (!v || !buf || !len || !view_is_shown(v)) + if (!v || !buf || !len || !cap || !view_is_shown(v)) return; if (v->kind == SZ_VIEW_EXCLUDE_SEMANTICS) return; @@ -1100,7 +1100,6 @@ static void a11y_dump_node(SzView *v, char *buf, size_t cap, size_t *len) { char line[256]; char live[256]; const char *label = v->a11y_label ? v->a11y_label : ""; - int n; if (v->kind == SZ_VIEW_TEXT && (v->sig_int || v->sig_str)) { resolve_text(v, live, sizeof live); label = live; @@ -1156,13 +1155,9 @@ static void a11y_dump_node(SzView *v, char *buf, size_t cap, size_t *len) { snprintf(live, sizeof live, "%d", view_overlay_open(v) ? 1 : 0); label = live; } - n = snprintf(line, sizeof line, "%s:%s\n", a11y_role_name(v->a11y_role), - label); - if (n > 0 && *len + (size_t)n < cap) { - memcpy(buf + *len, line, (size_t)n); - *len += (size_t)n; - buf[*len] = '\0'; - } + snprintf(line, sizeof line, "%s:%s\n", a11y_role_name(v->a11y_role), + label); + sz_dump_append(buf, len, cap, line); } if (v->kind == SZ_VIEW_MERGE_SEMANTICS) return; @@ -1173,15 +1168,18 @@ static void a11y_dump_node(SzView *v, char *buf, size_t cap, size_t *len) { if (v->kind == SZ_VIEW_OVERLAY && !view_overlay_open(v)) return; for (i = 0; i < v->child_count; i++) - a11y_dump_node(v->children[i], buf, cap, len); + a11y_dump_node(v->children[i], buf, len, cap); } SzString *sz_view_a11y_dump(SzView *root) { - char buf[4096]; - size_t len = 0; - buf[0] = '\0'; - a11y_dump_node(root, buf, sizeof buf, &len); - return sz_string_from_cstr(buf); + char *buf = NULL; + size_t len = 0, cap = 0; + SzString *out; + sz_dump_append(&buf, &len, &cap, ""); + a11y_dump_node(root, &buf, &len, &cap); + out = sz_string_from_cstr(buf); + sz_free(buf); + return out; } SzView *sz_view_column(void) { return view_new(SZ_VIEW_COLUMN); } @@ -1217,6 +1215,20 @@ SzView *sz_view_each_map(SzSignalList *sig, SzViewEachFn fn, void *env) { return v; } +static int each_seen_is_sentinel(SzList *p) { + return p == (SzList *)(uintptr_t)1; +} + +static void each_seen_set(SzView *v, SzList *xs) { + if (!v) + return; + if (v->each_seen && !each_seen_is_sentinel(v->each_seen)) + sz_release(v->each_seen); + if (xs) + sz_retain(xs); + v->each_seen = xs; +} + static void sync_each(SzView *v) { SzList *xs; SzList *p; @@ -1240,7 +1252,7 @@ static void sync_each(SzView *v) { sz_view_add_child(v, sz_view_text("- ")); } } - v->each_seen = xs; + each_seen_set(v, xs); } SzView *sz_view_scroll(SzView *child) { @@ -1523,6 +1535,9 @@ void sz_view_free(SzView *view) { view->tap_env = NULL; sz_release(view->each_env); view->each_env = NULL; + if (view->each_seen && view->each_seen != (SzList *)(uintptr_t)1) + sz_release(view->each_seen); + view->each_seen = NULL; { int u; for (u = 0; u < view->undo_n; u++) @@ -3702,11 +3717,6 @@ static void paint_ring_frac(SkCanvas *c, SzRect f, float t, float frac, static void paint_string(SkCanvas *c, const char *s, float x, float y, uint32_t argb, float font_px) { SkPaint *p; - if (g_clip_on) { - if (x >= g_clip.x + g_clip.w || y < g_clip.y || - y - font_px > g_clip.y + g_clip.h) - return; - } p = sk_paint_new(); if (!p) return; @@ -3719,11 +3729,6 @@ static void paint_string(SkCanvas *c, const char *s, float x, float y, static void paint_mono_string(SkCanvas *c, const char *s, float x, float y, uint32_t argb, float font_px) { SkPaint *p; - if (g_clip_on) { - if (x >= g_clip.x + g_clip.w || y < g_clip.y || - y - font_px > g_clip.y + g_clip.h) - return; - } p = sk_paint_new(); if (!p) return; @@ -4040,8 +4045,11 @@ static void paint_children_clipped(SzView *v, SkCanvas *c, const SzTheme *theme) g_clip = v->frame; g_clip_on = 1; } + sk_canvas_save(c); + sk_canvas_clip_rect(c, v->frame.x, v->frame.y, v->frame.w, v->frame.h); for (i = 0; i < v->child_count; i++) paint_node(v->children[i], c, theme); + sk_canvas_restore(c); g_clip = prev; g_clip_on = prev_on; } @@ -4698,6 +4706,8 @@ static void paint_node(SzView *v, SkCanvas *c, const SzTheme *theme) { g_clip = v->frame; g_clip_on = 1; } + sk_canvas_save(c); + sk_canvas_clip_rect(c, v->frame.x, v->frame.y, v->frame.w, v->frame.h); paint_rect(c, v->frame.x, v->frame.y, v->frame.w, v->frame.h, theme->surface); if (gutter > 0.f) paint_rect(c, v->frame.x, v->frame.y, gutter, v->frame.h, theme->background); @@ -4826,6 +4836,7 @@ static void paint_node(SzView *v, SkCanvas *c, const SzTheme *theme) { if (caret.w > 0.f) paint_rect(c, caret.x, caret.y, caret.w, caret.h, theme->primary); } + sk_canvas_restore(c); g_clip = prev_clip; g_clip_on = prev_on; break; diff --git a/crates/runtime/tests/test_io.c b/crates/runtime/tests/test_io.c index e9981a25..ee755729 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -1,4 +1,8 @@ +#define _DEFAULT_SOURCE #define _POSIX_C_SOURCE 200809L +#if defined(__APPLE__) +#define _DARWIN_C_SOURCE +#endif #include "scuzz_rt.h" #include @@ -12,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +24,13 @@ static SzIo *pure_drop(void *value); +static SzString *test_list_join(SzList *xs, const char *sep) { + SzString *s = sz_string_from_cstr(sep ? sep : ""); + SzString *out = sz_list_join(xs, s); + sz_release(s); + return out; +} + static void sleep_us(long us) { struct timespec ts; if (us <= 0) @@ -28,6 +40,92 @@ static void sleep_us(long us) { nanosleep(&ts, NULL); } +static int pid_is_zombie(pid_t pid) { +#ifdef __linux__ + char path[64]; + char buf[512]; + FILE *f; + char *rp; + if (pid <= 0) + return 0; + snprintf(path, sizeof path, "/proc/%d/stat", (int)pid); + f = fopen(path, "r"); + if (!f) + return 0; + if (!fgets(buf, (int)sizeof buf, f)) { + fclose(f); + return 0; + } + fclose(f); + rp = strrchr(buf, ')'); + return rp && rp[1] == ' ' && rp[2] == 'Z'; +#else + (void)pid; + return 0; +#endif +} + +#ifdef __linux__ +static int kill_reap_sleep_children(void) { + DIR *d; + struct dirent *ent; + pid_t self = getpid(); + int killed = 0; + d = opendir("/proc"); + if (!d) + return 0; + while ((ent = readdir(d))) { + pid_t pid; + char path[64]; + char buf[512]; + char line[256]; + FILE *f; + char *rp; + int ppid = 0; + size_t n; + size_t i; + if (ent->d_name[0] < '0' || ent->d_name[0] > '9') + continue; + pid = (pid_t)atoi(ent->d_name); + if (pid <= 0 || pid == self) + continue; + snprintf(path, sizeof path, "/proc/%d/stat", (int)pid); + f = fopen(path, "r"); + if (!f) + continue; + if (!fgets(buf, (int)sizeof buf, f)) { + fclose(f); + continue; + } + fclose(f); + rp = strrchr(buf, ')'); + if (!rp || sscanf(rp + 2, "%*c %d", &ppid) != 1 || ppid != (int)self) + continue; + snprintf(path, sizeof path, "/proc/%d/cmdline", (int)pid); + f = fopen(path, "r"); + if (!f) + continue; + n = fread(line, 1, sizeof line - 1, f); + fclose(f); + line[n] = '\0'; + for (i = 0; i < n; i++) { + if (line[i] == '\0') + line[i] = ' '; + } + if (!strstr(line, "sleep")) + continue; + (void)kill(pid, SIGKILL); + { + int st = 0; + (void)waitpid(pid, &st, 0); + } + killed++; + } + closedir(d); + return killed; +} +#endif + static uint16_t test_rd16(const uint8_t *p) { return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); } @@ -295,6 +393,32 @@ static void *stream_list_dup(void *v, void *env) { return two; } +static void *stream_list_keep(void *v, void *env) { + SzList *nil; + SzList *one; + (void)env; + if (sz_string_len((SzString *)v) == 0) + return sz_list_nil(); + nil = sz_list_nil(); + one = sz_list_cons(v, nil); + sz_release(nil); + return one; +} + +static void *stream_emit_keep(void *v, void *env) { + (void)env; + if (sz_string_len((SzString *)v) == 0) + return sz_stream_nil(); + return sz_stream_emit(v); +} + +static int iterate_calls = 0; +static void *stream_inc_count(void *v, void *env) { + (void)env; + iterate_calls++; + return sz_box_i64(sz_unbox_i64(v) + 1); +} + static void *stream_zip_concat(void *pack, void *env) { SzPair *p = (SzPair *)pack; (void)env; @@ -1866,6 +1990,14 @@ static void json_expect_stringify_err(SzAdt *j) { sz_release(j); } +static void expect_fs_refused_root(const char *path) { + SzIoResult r = sz_io_unsafe_run(sz_fs_delete(sz_string_from_cstr(path))); + assert(!r.ok); + assert(r.error != NULL); + assert(strstr(sz_string_cstr(r.error->message), "refused root") != NULL); + sz_error_free(r.error); +} + int main(void) { /* List.head_opt: None on empty, Some payload on a cell. C List.head still panics. */ { @@ -3228,6 +3360,20 @@ int main(void) { sz_error_free(r.error); sz_deferred_free(d3); } + { + size_t kb0 = 0, kc0 = 0, kb1 = 0, kc1 = 0; + SzDeferred *dc; + sz_alloc_kind_stats(SZ_RC_DEFERRED, &kb0, &kc0); + dc = sz_deferred_make(); + r = sz_io_unsafe_run(fm_drop( + fork_drop(sz_deferred_get(dc)), fiber_interrupt_then_join, NULL)); + assert(r.ok); + sz_deferred_free(dc); + sz_alloc_kind_stats(SZ_RC_DEFERRED, &kb1, &kc1); + assert(kc1 == kc0); + (void)kb0; + (void)kb1; + } sz_deferred_free(def); } @@ -3980,7 +4126,7 @@ int main(void) { sz_stream_eval(pure_drop(sz_string_from_cstr("c")))); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - SzString * joined = sz_list_join((SzList *)r.value, ","); + SzString * joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a!,b!,c") == 0); r = sz_io_unsafe_run(sz_stream_drain(sz_stream_emit(sz_string_from_cstr("d")))); @@ -3993,7 +4139,7 @@ int main(void) { r = sz_io_unsafe_run( sz_stream_compile_to_list(sz_stream_take(sz_stream_emits(xs), 2))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); delay_calls = 0; @@ -4003,7 +4149,7 @@ int main(void) { 1); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 1); @@ -4014,7 +4160,7 @@ int main(void) { r = sz_io_unsafe_run( sz_stream_compile_to_list(sz_stream_drop(sz_stream_emits(xs), 1))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "b,c") == 0); delay_calls = 0; @@ -4024,7 +4170,7 @@ int main(void) { 1); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "b") == 0); assert(delay_calls == 2); @@ -4035,7 +4181,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_filter(sz_stream_emits(xs), stream_nonempty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); delay_calls = 0; @@ -4047,7 +4193,7 @@ int main(void) { stream_nonempty, NULL); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); assert(delay_calls == 3); @@ -4063,7 +4209,7 @@ int main(void) { 1); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 1); @@ -4073,7 +4219,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_map(sz_stream_emits(xs), stream_bang_sync, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a!,b!") == 0); delay_calls = 0; @@ -4085,7 +4231,7 @@ int main(void) { 1); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a!") == 0); assert(delay_calls == 1); @@ -4098,7 +4244,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_takewhile(sz_stream_emits(xs), stream_nonempty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); delay_calls = 0; @@ -4110,7 +4256,7 @@ int main(void) { stream_nonempty, NULL); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 2); @@ -4123,7 +4269,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_dropwhile(sz_stream_emits(xs), stream_empty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); delay_calls = 0; @@ -4135,7 +4281,7 @@ int main(void) { stream_empty, NULL); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 3); @@ -4151,7 +4297,7 @@ int main(void) { 1); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 2); @@ -4162,7 +4308,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_find(sz_stream_emits(xs), stream_nonempty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); xs = sz_list_cons(sz_string_from_cstr(""), @@ -4190,7 +4336,7 @@ int main(void) { stream_nonempty, NULL); r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 2); @@ -4233,7 +4379,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_intersperse(sz_stream_emits(xs), sz_string_from_cstr("|")))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,|,b") == 0); xs = sz_list_cons( @@ -4244,7 +4390,7 @@ int main(void) { sz_stream_grouped(sz_stream_emits(xs), 2))); assert(r.ok); assert(sz_list_len((SzList *)r.value) == 2); - joined = sz_list_join((SzList *)sz_list_head((SzList *)r.value), ""); + joined = test_list_join((SzList *)sz_list_head((SzList *)r.value), ""); assert(strcmp(sz_string_cstr(joined), "ab") == 0); r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_range(3, 6))); @@ -4252,11 +4398,27 @@ int main(void) { assert(sz_list_len((SzList *)r.value) == 3); assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 3); + { + size_t base_bytes = 0, base_count = 0; + size_t live_bytes = 0, live_count = 0; + SzStream *rng; + sz_alloc_stats(&base_bytes, &base_count); + rng = sz_stream_range(0, 1000000); + sz_alloc_stats(&live_bytes, &live_count); + assert(live_count - base_count <= 4); + r = sz_io_unsafe_run( + sz_stream_compile_to_list(sz_stream_take(rng, 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 0); + sz_release(rng); + } + xs = sz_list_cons(sz_string_from_cstr("x"), sz_list_nil()); r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_repeat_n(sz_stream_emits(xs), 3))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "x,x,x") == 0); { @@ -4281,7 +4443,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_interleave(sz_stream_emits(as), sz_stream_emits(bs)))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b,c") == 0); } @@ -4296,7 +4458,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_interleave(sz_stream_emits(as), sz_stream_emits(bs)))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b,c,d,e") == 0); } @@ -4306,7 +4468,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_flatmap(sz_stream_emits(xs), stream_dup, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,a,b,b") == 0); { @@ -4318,7 +4480,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_flatten(sz_stream_emits(nested)))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b,c") == 0); } @@ -4329,7 +4491,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_changes(sz_stream_emits(xs)))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); xs = sz_list_cons( @@ -4339,7 +4501,7 @@ int main(void) { sz_stream_emits(xs), sz_string_from_cstr(""), stream_scan_concat, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), ",a,ab") == 0); xs = sz_list_cons( @@ -4364,6 +4526,15 @@ int main(void) { assert(r.ok); assert(sz_unbox_i64(r.value) == 0); + delay_calls = 0; + r = sz_io_unsafe_run(sz_stream_forall( + sz_stream_concat(sz_stream_eval(sz_io_delay(take_hit, (void *)"")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"a"))), + stream_nonempty, NULL)); + assert(r.ok); + assert(sz_unbox_i64(r.value) == 0); + assert(delay_calls == 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_range(5, 5))); assert(r.ok); assert(sz_list_is_empty((SzList *)r.value)); @@ -4391,13 +4562,13 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_interleave( sz_stream_nil(), sz_stream_emit(sz_string_from_cstr("a"))))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_interleave( sz_stream_emit(sz_string_from_cstr("a")), sz_stream_nil()))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); r = sz_io_unsafe_run(sz_stream_compile_to_list( @@ -4411,7 +4582,7 @@ int main(void) { sz_list_nil())))), 3))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b,c") == 0); r = sz_io_unsafe_run(sz_stream_fold(sz_stream_nil(), sz_string_from_cstr("z"), @@ -4439,15 +4610,92 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_filter_not(sz_stream_emits(xs), stream_empty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a,b") == 0); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_filter_not( + sz_stream_concat(sz_stream_eval(sz_io_delay(take_hit, (void *)"")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"a"))), + stream_empty, NULL), + 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a") == 0); + assert(delay_calls == 2); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_map_concat( + sz_stream_concat(sz_stream_eval(sz_io_delay(take_hit, (void *)"")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"a"))), + stream_list_keep, NULL), + 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a") == 0); + assert(delay_calls == 2); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_flatmap( + sz_stream_concat(sz_stream_eval(sz_io_delay(take_hit, (void *)"")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"a"))), + stream_emit_keep, NULL), + 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a") == 0); + assert(delay_calls == 2); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_changes(sz_stream_concat( + sz_stream_concat(sz_stream_eval(sz_io_delay(take_hit, (void *)"a")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"a"))), + sz_stream_eval(sz_io_delay(take_hit, (void *)"b")))), + 2); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); + assert(delay_calls == 3); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_filter( + sz_stream_evalmap( + sz_stream_concat( + sz_stream_eval(sz_io_delay(take_hit, (void *)"a")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"b"))), + stream_bang, NULL), + stream_nonempty, NULL), + 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a!") == 0); + assert(delay_calls == 1); + + delay_calls = 0; + s = sz_stream_take( + sz_stream_interleave(sz_stream_eval(sz_io_delay(take_hit, (void *)"a")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"b"))), + 1); + r = sz_io_unsafe_run(sz_stream_compile_to_list(s)); + assert(r.ok); + assert(delay_calls == 2); xs = sz_list_cons(sz_string_from_cstr("a"), sz_list_cons(sz_string_from_cstr("b"), sz_list_nil())); r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_map_concat(sz_stream_emits(xs), stream_list_dup, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,a,b,b") == 0); { @@ -4460,7 +4708,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_zip_with( sz_stream_emits(as), sz_stream_emits(bs), stream_zip_concat, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a1,b2") == 0); } @@ -4480,14 +4728,14 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_or_else( sz_stream_nil(), sz_stream_emit(sz_string_from_cstr("z"))))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "z") == 0); r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_or_else( sz_stream_emit(sz_string_from_cstr("a")), sz_stream_emit(sz_string_from_cstr("z"))))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a") == 0); xs = sz_list_cons( @@ -4506,7 +4754,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_take_right(sz_stream_emits(xs), 2))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "b,c") == 0); xs = sz_list_cons( @@ -4516,7 +4764,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_drop_right(sz_stream_emits(xs), 1))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); xs = sz_list_cons( @@ -4526,7 +4774,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_find_last(sz_stream_emits(xs), stream_nonempty, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "b") == 0); tap_n = 0; @@ -4535,7 +4783,7 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_evaltap(sz_stream_emits(xs), stream_tap_count, NULL))); assert(r.ok); - joined = sz_list_join((SzList *)r.value, ","); + joined = test_list_join((SzList *)r.value, ","); assert(strcmp(sz_string_cstr(joined), "a,b") == 0); assert(tap_n == 2); @@ -4548,6 +4796,14 @@ int main(void) { assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 0); } + iterate_calls = 0; + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_take( + sz_stream_iterate(sz_box_i64(0), 100, stream_inc_count, NULL), 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 0); + assert(iterate_calls == 0); + r = sz_io_unsafe_run(sz_stream_compile_to_list( sz_stream_iterate(sz_box_i64(0), 0, stream_inc, NULL))); assert(r.ok); @@ -4571,6 +4827,14 @@ int main(void) { r = sz_io_unsafe_run(sz_stream_head(sz_stream_nil())); assert(!r.ok); + delay_calls = 0; + r = sz_io_unsafe_run(sz_stream_head(sz_stream_concat( + sz_stream_eval(sz_io_delay(take_hit, (void *)"a")), + sz_stream_eval(sz_io_delay(take_hit, (void *)"b"))))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "a") == 0); + assert(delay_calls == 1); + xs = sz_list_cons(sz_string_from_cstr("a"), sz_list_cons(sz_string_from_cstr("b"), sz_list_nil())); r = sz_io_unsafe_run(sz_stream_last(sz_stream_emits(xs))); @@ -4952,6 +5216,22 @@ int main(void) { assert(strcmp(sz_string_cstr(right), "axx") == 0); assert(strcmp(sz_string_cstr(shortp), "abcd") == 0); assert(strcmp(sz_string_cstr(nopad), "a") == 0); + { + SzString *e = sz_string_from_cstr("\xc3\xa9"); + SzString *px = sz_string_from_cstr("x"); + SzString *p3 = sz_string_pad_left(e, 3, px); + SzString *a = sz_string_from_cstr("a"); + SzString *mb = sz_string_pad_left(a, 4, e); + assert(strcmp(sz_string_cstr(p3), "xx\xc3\xa9") == 0); + assert(sz_string_ulen(p3) == 3); + assert(sz_string_ulen(mb) == 4); + assert(sz_string_len(mb) == 7); + sz_string_free(e); + sz_string_free(px); + sz_string_free(p3); + sz_string_free(a); + sz_string_free(mb); + } assert(sz_string_is_blank(sz_string_from_cstr(" \t\n")) == 1); assert(sz_string_is_blank(sz_string_from_cstr(" a")) == 0); assert(sz_string_is_blank(NULL) == 1); @@ -5008,6 +5288,37 @@ int main(void) { assert(sz_string_ulen(tr) == 2 && sz_string_uchar_at(tr, 0) == 0xE9); assert(strcmp(sz_string_cstr(drr), "a") == 0); assert(sz_string_ulen(rv) == 3 && sz_string_uchar_at(rv, 0) == 'z' && sz_string_uchar_at(rv, 1) == 0xE9); + { + SzString *eonly = sz_string_uslice(u, 1, 2); + SzString *mid = sz_string_slice(eonly, 1, 2); + assert(sz_string_len(mid) == 1); + assert(sz_string_is_empty(mid) == 0); + assert(sz_string_ulen(mid) == 1); + assert(sz_string_uchar_at(mid, 0) == 0xA9); + assert(sz_string_uchar_at(mid, 1) == -1); + sz_string_free(mid); + sz_string_free(eonly); + } + { + SzString *eonly = sz_string_uslice(u, 1, 2); + SzString *px = sz_string_from_cstr("x"); + SzString *p3 = sz_string_pad_left(eonly, 3, px); + SzString *aone = sz_string_uslice(u, 0, 1); + SzString *mb = sz_string_pad_left(aone, 4, eonly); + assert(sz_string_ulen(p3) == 3); + assert(sz_string_uchar_at(p3, 0) == 'x'); + assert(sz_string_uchar_at(p3, 1) == 'x'); + assert(sz_string_uchar_at(p3, 2) == 0xE9); + assert(sz_string_ulen(mb) == 4); + assert(sz_string_len(mb) == 7); + assert(sz_string_uchar_at(mb, 0) == 0xE9); + assert(sz_string_uchar_at(mb, 3) == 'a'); + sz_string_free(eonly); + sz_string_free(px); + sz_string_free(p3); + sz_string_free(aone); + sz_string_free(mb); + } { /* Sequential walk, then a lower index. Leading non-ASCII must stay O(n). */ SzString *fwd = sz_string_from_cstr("\xc3\xa9" "abcdefghijklmnopqrstuvwxyz"); @@ -5069,6 +5380,8 @@ int main(void) { assert(sz_string_len(tr) == 0); } assert(strcmp(sz_string_cstr(sz_string_from_int(42)), "42") == 0); + assert(strcmp(sz_string_cstr(sz_string_from_bool(1)), "true") == 0); + assert(strcmp(sz_string_cstr(sz_string_from_bool(0)), "false") == 0); assert(strcmp(sz_string_cstr(sz_string_from_float(1.5)), "1.5") == 0); assert(strcmp(sz_string_cstr(sz_string_from_float(2.0)), "2.0") == 0); assert(strcmp(sz_string_cstr(sz_string_from_float(-1.5)), "-1.5") == 0); @@ -5137,7 +5450,7 @@ int main(void) { assert(sz_list_len(xs) == 2); assert(strcmp(sz_string_cstr((SzString *)sz_list_head(xs)), "a") == 0); assert(strcmp(sz_string_cstr((SzString *)sz_list_at(xs, 1)), "b") == 0); - SzString *j = sz_list_join(xs, ","); + SzString *j = test_list_join(xs, ","); assert(strcmp(sz_string_cstr(j), "a,b") == 0); } @@ -5413,6 +5726,42 @@ int main(void) { sz_error_free(r.error); remove("build/test_fs_mkdirs_file"); remove(path); + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr(""))); + assert(r.ok); + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr("."))); + assert(r.ok); + } + + /* Live root-delete guard: throwaway dir only. Never delete / or the repo. */ + { + char tmpl[] = "/tmp/scuzz-fs-XXXXXX"; + char oldcwd[2048]; + char *tmp; + assert(getcwd(oldcwd, sizeof oldcwd) != NULL); + tmp = mkdtemp(tmpl); + assert(tmp != NULL); + assert(chdir(tmp) == 0); + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr("work"))); + assert(r.ok); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr("canary.txt"), sz_string_from_cstr("x"))); + assert(r.ok); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr("work/canary.txt"), sz_string_from_cstr("y"))); + assert(r.ok); + assert(chdir("work") == 0); + expect_fs_refused_root("./"); + assert(access("canary.txt", F_OK) == 0); + expect_fs_refused_root("/."); + expect_fs_refused_root(".."); + assert(access("canary.txt", F_OK) == 0); + assert(access("../canary.txt", F_OK) == 0); + r = sz_io_unsafe_run(sz_fs_delete(sz_string_from_cstr("../canary.txt"))); + assert(r.ok); + assert(access("../canary.txt", F_OK) != 0); + assert(chdir(oldcwd) == 0); + r = sz_io_unsafe_run(sz_fs_delete(sz_string_from_cstr(tmp))); + assert(r.ok); } /* TestRuntime: Fs graph built before install still uses mem-FS. */ @@ -5495,7 +5844,32 @@ int main(void) { r = sz_io_unsafe_run(sz_clock_real_time()); assert(r.ok); - assert(sz_unbox_i64(r.value) == t1); + assert(sz_unbox_i64(r.value) == sz_testrt_clock_now_ms()); + + /* Parked poller must not freeze fake-clock sleepers. */ + { + int fds[2]; + struct timespec w0, w1; + int64_t ct0, ct1; + long wall_ms; + assert(pipe(fds) == 0); + ct0 = sz_testrt_clock_now_ms(); + clock_gettime(CLOCK_MONOTONIC, &w0); + r = sz_io_unsafe_run(race_drop( + sz_io_poll_readable(fds[0]), + fm_drop(sz_io_sleep_ms(50), after_sleep_tag, + (void *)(intptr_t)50))); + clock_gettime(CLOCK_MONOTONIC, &w1); + assert(r.ok); + assert((intptr_t)r.value == 50); + ct1 = sz_testrt_clock_now_ms(); + assert(ct1 == ct0 + 50); + wall_ms = (long)((w1.tv_sec - w0.tv_sec) * 1000L + + (w1.tv_nsec - w0.tv_nsec) / 1000000L); + assert(wall_ms < 25); + close(fds[0]); + close(fds[1]); + } r = sz_io_unsafe_run(sz_random_next_int(10)); assert(r.ok); @@ -5517,6 +5891,44 @@ int main(void) { assert(saw_hi); } + { + int64_t a[32]; + int64_t b[32]; + int i; + int alt; + sz_testrt_random_install(42); + for (i = 0; i < 32; i++) { + r = sz_io_unsafe_run(sz_random_next_int(2)); + assert(r.ok); + a[i] = sz_unbox_i64(r.value); + assert(a[i] == 0 || a[i] == 1); + sz_release(r.value); + } + alt = 1; + for (i = 1; i < 32; i++) { + if (a[i] == a[i - 1]) + alt = 0; + } + assert(!alt); + sz_testrt_random_install(42); + for (i = 0; i < 32; i++) { + r = sz_io_unsafe_run(sz_random_next_int(2)); + assert(r.ok); + b[i] = sz_unbox_i64(r.value); + sz_release(r.value); + assert(b[i] == a[i]); + } + } + + r = sz_io_unsafe_run(sz_random_next_int(0)); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "bound <= 0") != NULL); + sz_error_free(r.error); + r = sz_io_unsafe_run(sz_random_next_int(-3)); + assert(!r.ok); + sz_error_free(r.error); + sz_alloc_stats(&base_bytes, &base_count); r = sz_io_unsafe_run(sz_random_next_int(10)); assert(r.ok); @@ -5660,6 +6072,57 @@ int main(void) { assert(r.error && strstr(sz_string_cstr(r.error->message), "path too deep") != NULL); sz_error_free(r.error); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr(deep), sz_string_from_cstr("z"))); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "path too deep") != NULL); + sz_error_free(r.error); + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr(deep))); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "path too deep") != NULL); + sz_error_free(r.error); + } + + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr(""))); + assert(r.ok); + expect_fs_refused_root("./"); + expect_fs_refused_root("/."); + expect_fs_refused_root(".."); + r = sz_io_unsafe_run(sz_fs_mkdirs(sz_string_from_cstr("a"))); + assert(r.ok); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr("a/./b.txt"), sz_string_from_cstr("dot"))); + assert(r.ok); + r = sz_io_unsafe_run(sz_fs_read(sz_string_from_cstr("a/b.txt"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "dot") == 0); + sz_release(r.value); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr("a/x"), sz_string_from_cstr("up"))); + assert(r.ok); + r = sz_io_unsafe_run(sz_fs_read(sz_string_from_cstr("a/../a/x"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "up") == 0); + sz_release(r.value); + r = sz_io_unsafe_run( + sz_fs_write(sz_string_from_cstr("a/./b"), sz_string_from_cstr("leaf"))); + assert(r.ok); + r = sz_io_unsafe_run(sz_fs_list(sz_string_from_cstr("a"))); + assert(r.ok); + { + SzList *xs = (SzList *)r.value; + int saw_b = 0; + while (xs && !sz_list_is_empty(xs)) { + SzPair *ent = (SzPair *)sz_list_head(xs); + SzString *name = (SzString *)sz_pair_left(ent); + if (strcmp(sz_string_cstr(name), "b") == 0) + saw_b = 1; + xs = sz_list_tail(xs); + } + assert(saw_b); + sz_release(r.value); } r = sz_io_unsafe_run( @@ -5926,6 +6389,38 @@ int main(void) { assert(r.ok); assert(strcmp(sz_string_cstr((SzString *)r.value), "hi") == 0); sz_release(r.value); + { + char blob[32]; + SzString *body32; + memset(blob, 'x', 32); + body32 = sz_string_from_bytes(blob, 32); + r = sz_io_unsafe_run(both_drop(sz_net_tcp_read(a, 8), + sz_net_tcp_write(b, body32))); + sz_release(body32); + assert(r.ok); + pair = (SzPair *)r.value; + assert(sz_string_len((SzString *)pair->left) == 8); + sz_pair_free(pair); + r = sz_io_unsafe_run(sz_net_tcp_read(a, 24)); + assert(r.ok); + assert(sz_string_len((SzString *)r.value) == 24); + sz_release(r.value); + } + { + char *blob = (char *)malloc(10000); + SzString *big; + assert(blob); + memset(blob, 'y', 10000); + big = sz_string_from_bytes(blob, 10000); + free(blob); + r = sz_io_unsafe_run(sz_net_tcp_write(b, big)); + sz_release(big); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_tcp_read(a, 10000)); + assert(r.ok); + assert(sz_string_len((SzString *)r.value) == 10000); + sz_release(r.value); + } r = sz_io_unsafe_run(sz_net_tcp_close(a)); assert(r.ok); r = sz_io_unsafe_run(sz_net_tcp_close(b)); @@ -5959,6 +6454,48 @@ int main(void) { assert(sz_unbox_i64(inner->left) == 19092); } sz_pair_free(pair); + { + SzString *host = sz_string_from_cstr("127.0.0.1"); + SzString *ma = sz_string_from_cstr("a"); + SzString *mb = sz_string_from_cstr("b"); + r = sz_io_unsafe_run(sz_net_udp_send(a, host, 19093, ma)); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_udp_send(a, host, 19093, mb)); + assert(r.ok); + sz_release(ma); + sz_release(mb); + r = sz_io_unsafe_run(sz_net_udp_recv(b, 32)); + assert(r.ok); + pair = (SzPair *)r.value; + assert(strcmp(sz_string_cstr((SzString *)((SzPair *)pair->right)->right), + "a") == 0); + sz_pair_free(pair); + r = sz_io_unsafe_run(sz_net_udp_recv(b, 32)); + assert(r.ok); + pair = (SzPair *)r.value; + assert(strcmp(sz_string_cstr((SzString *)((SzPair *)pair->right)->right), + "b") == 0); + sz_pair_free(pair); + { + SzString *bad = sz_string_from_cstr("nope"); + SzString *msg = sz_string_from_cstr("x"); + r = sz_io_unsafe_run(sz_net_udp_send(a, bad, 19093, msg)); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "IP literal") != NULL); + sz_error_free(r.error); + sz_release(bad); + bad = sz_string_from_cstr("::1"); + r = sz_io_unsafe_run(sz_net_udp_send(a, bad, 19093, msg)); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "send failed") != NULL); + sz_error_free(r.error); + sz_release(bad); + sz_release(msg); + } + sz_release(host); + } r = sz_io_unsafe_run(sz_net_udp_close(a)); assert(r.ok); r = sz_io_unsafe_run(sz_net_udp_close(b)); @@ -6048,6 +6585,50 @@ int main(void) { assert(strcmp(sz_string_cstr((SzString *)r.value), "sealed") == 0); sz_release(r.value); + /* Drive.testEnv keys seed the sealed map. Host PATH still does not leak. */ + { + const char *old_serve = getenv("SCUZZ_SERVE"); + const char *old_kit = getenv("SCUZZ_KIT"); + char serve_save[64]; + char kit_save[64]; + int had_serve = old_serve && old_serve[0]; + int had_kit = old_kit && old_kit[0]; + if (had_serve) + snprintf(serve_save, sizeof serve_save, "%s", old_serve); + if (had_kit) + snprintf(kit_save, sizeof kit_save, "%s", old_kit); + setenv("SCUZZ_SERVE", "1", 1); + setenv("SCUZZ_KIT", "sealed", 1); + sz_testrt_install(); + r = sz_io_unsafe_run(sz_sys_getenv(sz_string_from_cstr("SCUZZ_SERVE"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "1") == 0); + sz_release(r.value); + r = sz_io_unsafe_run(sz_sys_getenv(sz_string_from_cstr("SCUZZ_KIT"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "sealed") == 0); + sz_release(r.value); + r = sz_io_unsafe_run(sz_sys_getenv(sz_string_from_cstr("PATH"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "") == 0); + sz_release(r.value); + unsetenv("SCUZZ_SERVE"); + unsetenv("SCUZZ_KIT"); + sz_testrt_install(); + r = sz_io_unsafe_run(sz_sys_getenv(sz_string_from_cstr("SCUZZ_SERVE"))); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "") == 0); + sz_release(r.value); + if (had_serve) + setenv("SCUZZ_SERVE", serve_save, 1); + else + unsetenv("SCUZZ_SERVE"); + if (had_kit) + setenv("SCUZZ_KIT", kit_save, 1); + else + unsetenv("SCUZZ_KIT"); + } + { size_t base_bytes = 0, base_count = 0; size_t live_bytes = 0, live_count = 0; @@ -6227,6 +6808,24 @@ int main(void) { } sz_deferred_free(def); + /* Two parked gets both wake. Oldest waiter is first in the list. */ + def = sz_deferred_make(); + r = sz_io_unsafe_run(both_drop( + both_drop(sz_deferred_get(def), sz_deferred_get(def)), + sz_deferred_complete_cstr(def, "go"))); + assert(r.ok); + { + SzPair *outer = (SzPair *)r.value; + SzPair *gets; + assert(outer); + gets = (SzPair *)outer->left; + assert(gets); + assert(strcmp(sz_string_cstr((SzString *)gets->left), "go") == 0); + assert(strcmp(sz_string_cstr((SzString *)gets->right), "go") == 0); + sz_pair_free(outer); + } + sz_deferred_free(def); + sz_testrt_reset(); } @@ -6447,6 +7046,42 @@ int main(void) { assert(sz_unbox_i64(r.value) == 0); } + /* Sys.kill reaps without Sys.alive. Watch kills then spawns. */ + { + SzIoResult r; + SzIoResult sr; + int64_t pid; + int64_t pid2; + int i; + int status = 0; + int st2 = 0; + pid_t w; + r = sz_io_unsafe_run(sz_sys_spawn(sz_string_from_cstr("exec sleep 5"))); + assert(r.ok); + pid = sz_unbox_i64(r.value); + sz_release(r.value); + assert(pid > 0); + r = sz_io_unsafe_run(sz_sys_kill(pid)); + assert(r.ok); + for (i = 0; i < 50; i++) { + if (pid_is_zombie((pid_t)pid)) + break; + if (kill((pid_t)pid, 0) != 0 && errno == ESRCH) + break; + sleep_us(20000); + } + sr = sz_io_unsafe_run(sz_sys_spawn(sz_string_from_cstr("true"))); + assert(sr.ok); + pid2 = sz_unbox_i64(sr.value); + sz_release(sr.value); + assert(waitpid((pid_t)pid2, &st2, 0) == (pid_t)pid2); + assert(!pid_is_zombie((pid_t)pid)); + w = waitpid((pid_t)pid, &status, WNOHANG); + assert(w == (pid_t)pid || (w < 0 && errno == ECHILD)); + (void)sz_io_unsafe_run(sz_sys_alive(pid2)); + (void)sz_io_unsafe_run(sz_sys_alive(pid)); + } + /* Sys.spawn pipes: write stdin, read stdout (dd copies five bytes). */ { SzIoResult pr; @@ -6496,21 +7131,26 @@ int main(void) { assert(pr.ok); pr = sz_io_unsafe_run(sz_sys_child_close(pid)); assert(pr.ok); - pr = sz_io_unsafe_run(sz_sys_child_read(pid, 2)); + pr = sz_io_unsafe_run(sz_sys_child_read(pid, 3)); assert(pr.ok); got = (SzString *)pr.value; assert(got && got->len == 2); assert(memcmp(sz_string_cstr(got), "xy", 2) == 0); sz_release(got); - pr = sz_io_unsafe_run(sz_sys_child_read(pid, 1)); - assert(pr.ok); - got = (SzString *)pr.value; - assert(got && got->len == 0); - sz_release(got); pr = sz_io_unsafe_run(sz_sys_kill(pid)); assert(pr.ok); } + /* Sys.childRead on an unknown pid fails like Sys.childWrite. */ + { + SzIoResult pr; + pr = sz_io_unsafe_run(sz_sys_child_read(999999, 1)); + assert(!pr.ok); + assert(pr.error && + strstr(sz_string_cstr(pr.error->message), "unknown pid") != NULL); + sz_error_free(pr.error); + } + /* Child read parks; a peer fiber writes before the copy finishes. */ { SzIoResult pr; @@ -6590,6 +7230,7 @@ int main(void) { size_t base_bytes = 0, base_count = 0; size_t live_bytes = 0, live_count = 0; int status = 0; + (void)sz_io_unsafe_run(sz_sys_alive(0)); sz_alloc_stats(&base_bytes, &base_count); { SzString *cmd = sz_string_from_cstr("true"); @@ -6693,6 +7334,35 @@ int main(void) { assert(g_peer_flag == 1); } + /* Cancel of a long child reaps it. */ + { + SzString *cmd = sz_string_from_cstr("exec sleep 30"); + r = sz_io_unsafe_run(race_drop( + sz_sys_exec (cmd), + sz_io_sleep_ms(1000))); + sz_release(cmd); + assert(r.ok); + if (r.value) + sz_release(r.value); +#ifdef __linux__ + { + int leftover = kill_reap_sleep_children(); + assert(leftover == 0); + } +#endif + } + + /* Capture over 1 MiB fails. */ + { + SzString *cmd = sz_string_from_cstr("dd if=/dev/zero bs=1024 count=2048 2>/dev/null"); + r = sz_io_unsafe_run(sz_sys_exec (cmd)); + sz_release(cmd); + assert(!r.ok); + assert(r.error && + strstr(sz_string_cstr(r.error->message), "1 MiB") != NULL); + sz_error_free(r.error); + } + /* Live Net.serveOnce: client thread GET while this fiber accepts. */ { pthread_t th; @@ -6733,6 +7403,91 @@ int main(void) { sz_error_free(r.error); } + /* Live TCP echo and a 10000-byte read (drain until n, not a 4096 cap). */ + { + SzNetSock *ln; + SzNetSock *a; + SzNetSock *b; + SzPair *pair; + SzString *host = sz_string_from_cstr("127.0.0.1"); + SzString *hi = sz_string_from_cstr("hello"); + r = sz_io_unsafe_run(sz_net_tcp_listen(19111)); + assert(r.ok); + ln = (SzNetSock *)r.value; + r = sz_io_unsafe_run(both_drop(sz_net_tcp_accept(ln), + sz_net_tcp_connect(host, 19111))); + sz_release(host); + assert(r.ok); + pair = (SzPair *)r.value; + a = (SzNetSock *)pair->left; + b = (SzNetSock *)pair->right; + sz_retain(a); + sz_retain(b); + sz_pair_free(pair); + r = sz_io_unsafe_run(sz_net_tcp_write(b, hi)); + sz_release(hi); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_tcp_read(a, 16)); + assert(r.ok); + assert(strcmp(sz_string_cstr((SzString *)r.value), "hello") == 0); + sz_release(r.value); + { + char *blob = (char *)malloc(10000); + SzString *big; + assert(blob); + memset(blob, 'z', 10000); + big = sz_string_from_bytes(blob, 10000); + free(blob); + r = sz_io_unsafe_run(sz_net_tcp_write(b, big)); + sz_release(big); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_tcp_read(a, 10000)); + assert(r.ok); + assert(sz_string_len((SzString *)r.value) == 10000); + sz_release(r.value); + } + r = sz_io_unsafe_run(sz_net_tcp_close(a)); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_tcp_close(b)); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_tcp_close(ln)); + assert(r.ok); + sz_release(a); + sz_release(b); + sz_release(ln); + } + + /* Live UDP ping on IPv4 localhost. */ + { + SzNetSock *a; + SzNetSock *b; + SzPair *pair; + SzString *host = sz_string_from_cstr("127.0.0.1"); + SzString *msg = sz_string_from_cstr("ping"); + r = sz_io_unsafe_run(sz_net_udp_bind(19112)); + assert(r.ok); + a = (SzNetSock *)r.value; + r = sz_io_unsafe_run(sz_net_udp_bind(19113)); + assert(r.ok); + b = (SzNetSock *)r.value; + r = sz_io_unsafe_run(sz_net_udp_send(a, host, 19113, msg)); + sz_release(host); + sz_release(msg); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_udp_recv(b, 32)); + assert(r.ok); + pair = (SzPair *)r.value; + assert(strcmp(sz_string_cstr((SzString *)((SzPair *)pair->right)->right), + "ping") == 0); + sz_pair_free(pair); + r = sz_io_unsafe_run(sz_net_udp_close(a)); + assert(r.ok); + r = sz_io_unsafe_run(sz_net_udp_close(b)); + assert(r.ok); + sz_release(a); + sz_release(b); + } + /* Client connects and never sends GET: serve fails in ~1s instead of hanging. */ { pthread_t th; @@ -9096,8 +9851,15 @@ int main(void) { assert(sz_unbox_i64(sz_list_at(out, 1)) == 2); assert(sz_unbox_i64(sz_list_at(out, 2)) == 3); sz_list_free(out); + out = sz_list_sort(ns, 0); + assert(sz_unbox_i64(sz_list_head(out)) == 1); + assert(sz_unbox_i64(sz_list_at(out, 1)) == 2); + assert(sz_unbox_i64(sz_list_at(out, 2)) == 3); + sz_list_free(out); assert(sz_unbox_i64(sz_list_max(ns, 1)) == 3); assert(sz_unbox_i64(sz_list_min(ns, 1)) == 1); + assert(sz_unbox_i64(sz_list_max(ns, 0)) == 3); + assert(sz_unbox_i64(sz_list_min(ns, 0)) == 1); sz_list_free(ns); sz_release(n1); sz_release(n2); @@ -9579,17 +10341,37 @@ int main(void) { assert(sz_map_contains(m1, kb) == 0); assert(strcmp(sz_string_cstr((SzString *)sz_map_get_or(m2, ka, NULL)), "1") == 0); { - SzList *hit = sz_map_get(m2, ka); - SzList *miss = sz_map_get(m1, kb); + SzAdt *hit = (SzAdt *)sz_map_get(m2, ka); + SzAdt *miss = (SzAdt *)sz_map_get(m1, kb); SzString *kz = sz_string_from_cstr("z"); - SzList *absent = sz_map_get(m2, kz); - assert(sz_list_len(hit) == 1); - assert(strcmp(sz_string_cstr((SzString *)hit->head), "1") == 0); - assert(sz_list_is_empty(miss)); - assert(sz_list_is_empty(absent)); - sz_list_free(hit); + SzAdt *absent = (SzAdt *)sz_map_get(m2, kz); + assert(sz_adt_tag(hit) == 1); + assert(strcmp(sz_string_cstr((SzString *)sz_adt_payload(hit)), "1") == 0); + assert(sz_adt_tag(miss) == 0); + assert(sz_adt_tag(absent) == 0); + sz_release(hit); + sz_release(miss); + sz_release(absent); sz_string_free(kz); } + { + SzString *ke = sz_string_from_cstr("e"); + SzMap *empty = sz_map_set(NULL, ke, NULL, 1); + SzAdt *some; + SzString *kz = sz_string_from_cstr("z"); + SzAdt *none; + assert(sz_map_contains(empty, ke) == 1); + some = (SzAdt *)sz_map_get(empty, ke); + none = (SzAdt *)sz_map_get(empty, kz); + assert(sz_adt_tag(some) == 1); + assert(sz_adt_payload(some) == NULL); + assert(sz_adt_tag(none) == 0); + sz_release(some); + sz_release(none); + sz_string_free(kz); + sz_release(empty); + sz_string_free(ke); + } sz_release(m2); sz_release(m1); sz_string_free(ka); @@ -10787,6 +11569,37 @@ int main(void) { assert(sz_timeline_a11y_has(tl, 0, needle) == 1); sz_release(needle); sz_timeline_free(tl); + write_text(path, "# timeline v=2 n=1\n--- 0\nlast_hit:\n\ndrive:\n\n" + "signals:\nlist[1] q = [\"a\\\"b\", \"a\\nb\"]\n" + "str[2] draft = \"a\\\"b\"\na11y:\n\n"); + tl = sz_timeline_load(path); + assert(tl); + needle = sz_string_from_cstr("q"); + assert(sz_timeline_signal_list_len(tl, 0, needle) == 2); + sz_release(needle); + needle = sz_string_from_cstr("draft"); + { + SzString *want = sz_string_from_cstr("a\"b"); + assert(sz_timeline_signal_str_has(tl, 0, needle, want) == 1); + sz_release(want); + } + sz_release(needle); + sz_timeline_free(tl); + write_text(path, "# timeline v=2 n=1\n--- 0\nlast_hit:\n\ndrive:\n\n" + "signals:\nstr[2] draft = \"\"\na11y:\n\n"); + tl = sz_timeline_load(path); + assert(tl); + needle = sz_string_from_cstr("draft"); + { + SzString *want = sz_string_from_cstr(""); + assert(sz_timeline_signal_str_has(tl, 0, needle, want) == 1); + sz_release(want); + want = sz_string_from_cstr("x"); + assert(sz_timeline_signal_str_has(tl, 0, needle, want) == 0); + sz_release(want); + } + sz_release(needle); + sz_timeline_free(tl); write_text(path, "# timeline v=3 n=0\n"); assert(sz_timeline_load(path) == NULL); write_text(path, "nonsense\n"); diff --git a/crates/runtime/tests/test_ui.c b/crates/runtime/tests/test_ui.c index aa497f65..80ea22b3 100644 --- a/crates/runtime/tests/test_ui.c +++ b/crates/runtime/tests/test_ui.c @@ -11,6 +11,7 @@ #include #include #include +#include int sz_view_paint(SzView *root, SkCanvas *canvas, int width, int height, const SzTheme *theme); @@ -1003,6 +1004,56 @@ static void test_session_inject_script(void) { remove(path); } +static void test_session_inject_grows_past_4k(void) { + SzUiConfig cfg; + SzUiSession *session; + SzView *root, *btn; + SzSignalInt *count; + const char *path = "/tmp/scuzz_ui_inject_4k.script"; + FILE *f; + int i; + + remove(path); + count = sz_signal_int(0); + root = sz_view_column(); + btn = sz_view_button("+", counter_tap, count); + sz_view_add_child(root, btn); + + memset(&cfg, 0, sizeof(cfg)); + cfg.kind = SZ_UI_RUNTIME_HEADLESS; + cfg.width = 200; + cfg.height = 100; + cfg.scale = 1.0; + session = sz_ui_mount(&cfg, root); + assert(session); + sz_ui_session_take_root(session); + assert(sz_ui_session_set_inject(session, path)); + assert(sz_ui_pump_sync(session)); + assert(sz_signal_int_get(count) == 0); + + f = fopen(path, "w"); + assert(f); + fputc('#', f); + for (i = 0; i < 4200; i++) + fputc('x', f); + fputc('\n', f); + fputs("tap 0\n", f); + fclose(f); + assert(sz_ui_pump_sync(session)); + assert(sz_signal_int_get(count) == 1); + + f = fopen(path, "a"); + assert(f); + fputs("tap 0\n", f); + fclose(f); + assert(sz_ui_pump_sync(session)); + assert(sz_signal_int_get(count) == 2); + + sz_ui_unmount(session); + sz_signal_int_free(count); + remove(path); +} + static void test_session_inject_control(void) { SzUiConfig cfg; SzUiSession *session; @@ -1295,6 +1346,10 @@ static void test_session_inject_type(void) { assert(sz_ui_pump_sync(session)); assert(strcmp(sz_signal_str_get(draft), "abc") == 0); + write_stamp(path, "text x\ntype a\\nb\n"); + assert(sz_ui_pump_sync(session)); + assert(strcmp(sz_signal_str_get(draft), "xa\nb") == 0); + sz_ui_unmount(session); sz_signal_str_free(draft); remove(path); @@ -1444,6 +1499,46 @@ static void test_record_live_key(void) { remove(record); } +static void test_record_type_escapes(void) { + SzUiConfig cfg; + SzUiSession *session; + SzView *root, *field; + SzSignalStr *draft; + SzInputEvent ev; + const char *record = "/tmp/scuzz_ui_record_type_esc.script"; + char *body; + + remove(record); + draft = sz_signal_str(""); + root = sz_view_column(); + field = sz_view_text_field(draft, "item"); + sz_view_add_child(root, field); + + memset(&cfg, 0, sizeof(cfg)); + cfg.kind = SZ_UI_RUNTIME_HEADLESS; + cfg.width = 200; + cfg.height = 80; + cfg.scale = 1.0; + session = sz_ui_mount(&cfg, root); + assert(session); + sz_ui_session_take_root(session); + assert(sz_ui_session_set_record(session, record)); + assert(sz_ui_pump_sync(session)); + + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_TEXT_EDIT; + ev.text = "a\nb"; + assert(sz_ui_session_live_inject(session, &ev)); + body = slurp_cstr(record); + assert(strstr(body, "type a\\nb") != NULL); + free(body); + assert(strcmp(sz_signal_str_get(draft), "a\nb") == 0); + + sz_ui_unmount(session); + sz_signal_str_free(draft); + remove(record); +} + static void test_session_inject_key_repeat(void) { SzUiConfig cfg; SzUiSession *session; @@ -2660,47 +2755,11 @@ static void test_clip_sizes_to_child(void) { sz_view_free(clip); } -/* Skia N32 peek is RGBA on Darwin and BGRA on Linux. Map R/B once. */ -static int g_px_r = 0; -static int g_px_b = 2; -static int g_px_mapped; - -static void px_map_channels(void) { - SkSurface *surf; - SkCanvas *canvas; - SkPaint *paint; - const uint8_t *px; - size_t n = 0; - if (g_px_mapped) - return; - g_px_mapped = 1; - surf = sk_surface_make_raster_n32_premul(4, 4); - if (!surf) - return; - canvas = sk_surface_get_canvas(surf); - paint = sk_paint_new(); - if (!canvas || !paint) { - if (paint) - sk_paint_delete(paint); - sk_surface_unref(surf); - return; - } - sk_paint_set_color(paint, sk_color_rgba(255, 0, 0, 255)); - sk_canvas_draw_rect(canvas, 0, 0, 4, 4, paint); - px = sk_surface_peek_pixels(surf, &n); - if (px && n >= 4 && px[2] > 200 && px[0] < 50) { - g_px_r = 2; - g_px_b = 0; - } - sk_paint_delete(paint); - sk_surface_unref(surf); -} - +/* Peek is row-major RGBA. */ static int px_rgb(const uint8_t *px, int w, int x, int y, uint8_t r, uint8_t g, uint8_t b) { const uint8_t *p = px + ((size_t)y * (size_t)w + (size_t)x) * 4; - px_map_channels(); - return p[g_px_r] == r && p[1] == g && p[g_px_b] == b; + return p[0] == r && p[1] == g && p[2] == b; } static void test_clip_paint_contains_overflow(void) { @@ -4434,10 +4493,9 @@ static void test_text_color_keeps_a11y(void) { static int row_has_red(const uint8_t *px, int w, int y, int x0, int x1) { int x; - px_map_channels(); for (x = x0; x < x1; x++) { const uint8_t *p = px + ((size_t)y * (size_t)w + (size_t)x) * 4; - if (p[g_px_r] > 180 && p[1] < 80 && p[g_px_b] < 80) + if (p[0] > 180 && p[1] < 80 && p[2] < 80) return 1; } return 0; @@ -4445,10 +4503,9 @@ static int row_has_red(const uint8_t *px, int w, int y, int x0, int x1) { static int row_has_blue(const uint8_t *px, int w, int y, int x0, int x1) { int x; - px_map_channels(); for (x = x0; x < x1; x++) { const uint8_t *p = px + ((size_t)y * (size_t)w + (size_t)x) * 4; - if (p[g_px_b] > 180 && p[g_px_r] < 80 && p[1] < 80) + if (p[2] > 180 && p[0] < 80 && p[1] < 80) return 1; } return 0; @@ -12318,6 +12375,49 @@ static void test_slider_pointer_drag(void) { sz_signal_int_free(sig); } +static void test_replace_root_drops_pointer(void) { + SzUiConfig cfg; + SzUiSession *session; + SzView *root1, *root2, *sl; + SzSignalInt *sig; + SzInputEvent ev; + const SzTheme *theme = sz_theme_default(); + SzRect f; + int64_t v0; + + sig = sz_signal_int(0); + sl = sz_view_slider(sig); + root1 = sz_view_column(); + sz_view_add_child(root1, sl); + memset(&cfg, 0, sizeof(cfg)); + cfg.kind = SZ_UI_RUNTIME_HEADLESS; + cfg.width = 200; + cfg.height = 80; + cfg.scale = 1.0; + session = sz_ui_mount(&cfg, root1); + assert(session); + sz_ui_session_take_root(session); + assert(sz_ui_pump_sync(session)); + sz_view_layout(root1, 200.f, 80.f, theme); + f = sz_view_frame(sl); + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_POINTER; + ev.pointer_phase = SZ_POINTER_DOWN; + ev.x = f.x + 4.f; + ev.y = f.y + f.h * 0.5f; + assert(sz_ui_inject_sync(session, &ev)); + v0 = sz_signal_int_get(sig); + root2 = sz_view_column(); + sz_view_add_child(root2, sz_view_text("after")); + assert(sz_ui_session_replace_root(session, root2)); + ev.pointer_phase = SZ_POINTER_MOVE; + ev.x = f.x + f.w * 0.8f; + (void)sz_ui_inject_sync(session, &ev); + assert(sz_signal_int_get(sig) == v0); + sz_ui_unmount(session); + sz_signal_int_free(sig); +} + static void test_slider_live_records_xy(void) { SzUiConfig cfg; SzUiSession *session; @@ -12845,6 +12945,28 @@ static void test_a11y(void) { } } +static void test_a11y_dump_grows_past_4k(void) { + SzView *col; + SzString *dump; + char lab[64]; + int i; + const char *s; + + col = sz_view_column(); + for (i = 0; i < 80; i++) { + snprintf(lab, sizeof lab, "L%02d-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + i); + sz_view_add_child(col, sz_view_button(lab, NULL, NULL)); + } + sz_view_add_child(col, sz_view_button("TAIL", NULL, NULL)); + dump = sz_view_a11y_dump(col); + s = sz_string_cstr(dump); + assert(strlen(s) > 4096); + assert(strstr(s, "button:TAIL") != NULL); + sz_string_free(dump); + sz_view_free(col); +} + static void test_clear_children(void) { SzView *list; SzString *dump; @@ -12893,6 +13015,39 @@ static void test_view_each(void) { sz_signal_list_free(items); } +static void test_view_each_setlist_rebuilds(void) { + SzSignalList *items; + SzView *list; + const SzTheme *theme = sz_theme_default(); + SzList *xs; + SzString *dump; + int i; + + xs = sz_list_cons(sz_string_from_cstr("old"), sz_list_nil()); + items = sz_signal_list(xs); + sz_release(xs); + list = sz_view_each(items); + sz_view_layout(list, 200.f, 120.f, theme); + dump = sz_view_a11y_dump(list); + assert(strstr(sz_string_cstr(dump), "text:- old") != NULL); + sz_string_free(dump); + + xs = sz_list_cons(sz_string_from_cstr("new"), sz_list_nil()); + sz_signal_list_set(items, xs); + sz_release(xs); + for (i = 0; i < 64; i++) { + SzList *t = sz_list_cons(sz_string_from_cstr("churn"), sz_list_nil()); + sz_release(t); + } + sz_view_layout(list, 200.f, 120.f, theme); + dump = sz_view_a11y_dump(list); + assert(strstr(sz_string_cstr(dump), "text:- new") != NULL); + assert(strstr(sz_string_cstr(dump), "text:- old") == NULL); + sz_string_free(dump); + sz_view_free(list); + sz_signal_list_free(items); +} + static SzView *each_map_text(SzString *item, void *env) { (void)env; return sz_view_text(item ? sz_string_cstr(item) : ""); @@ -13186,6 +13341,119 @@ static void test_property_signal_str(void) { sz_signal_str_free(draft); } +static void test_signal_dump_escapes(void) { + SzSignalStr *s; + SzSignalList *items; + SzList *xs; + SzString *dump; + SzString *name; + SzString *got; + const char *d; + char big[2001]; + int i; + + s = sz_signal_str("a\"b"); + sz_signal_name(s, "q"); + dump = sz_signal_dump(); + d = sz_string_cstr(dump); + assert(strstr(d, "a\\\"b") != NULL); + sz_string_free(dump); + + sz_signal_str_set(s, "a\nb"); + dump = sz_signal_dump(); + d = sz_string_cstr(dump); + assert(strstr(d, "a\\nb") != NULL); + sz_string_free(dump); + + for (i = 0; i < 2000; i++) + big[i] = 'x'; + big[2000] = '\0'; + sz_signal_str_set(s, big); + dump = sz_signal_dump(); + d = sz_string_cstr(dump); + assert(strlen(d) > 2000); + assert(strstr(d, big) != NULL); + sz_string_free(dump); + sz_signal_str_free(s); + + xs = sz_list_cons(sz_string_from_cstr("a\"b"), + sz_list_cons(sz_string_from_cstr("a\nb"), sz_list_nil())); + items = sz_signal_list(xs); + sz_signal_name(items, "xs"); + dump = sz_signal_dump(); + d = sz_string_cstr(dump); + assert(strstr(d, "a\\\"b") != NULL); + assert(strstr(d, "a\\nb") != NULL); + name = sz_string_from_cstr("xs"); + assert(sz_property_signal_list_len(name) == 2); + got = sz_property_signal_list_at(name, 0); + assert(strcmp(sz_string_cstr(got), "a\"b") == 0); + sz_string_free(got); + got = sz_property_signal_list_at(name, 1); + assert(strcmp(sz_string_cstr(got), "a\nb") == 0); + sz_string_free(got); + sz_release(name); + sz_string_free(dump); + sz_signal_list_free(items); +} + +static void test_signal_name_last_wins(void) { + SzSignalInt *a; + SzSignalInt *b; + SzString *name; + + a = sz_signal_int(1); + b = sz_signal_int(2); + sz_signal_name(a, "n"); + sz_signal_name(b, "n"); + name = sz_string_from_cstr("n"); + assert(sz_property_signal_int(name) == 2); + sz_release(name); + sz_signal_int_free(a); + sz_signal_int_free(b); +} + +static void test_property_replay_str_list(void) { + SzSignalStr *draft; + SzSignalList *items; + SzList *xs; + SzString *name; + SzString *got; + + setenv("SCUZZ_TESTRT", "1", 1); + sz_property_session_reset(); + draft = sz_signal_str("old"); + sz_signal_name(draft, "draft"); + xs = sz_list_cons(sz_string_from_cstr("a"), + sz_list_cons(sz_string_from_cstr("b"), sz_list_nil())); + items = sz_signal_list(xs); + sz_signal_name(items, "items"); + sz_property_session_step(); + sz_signal_str_set(draft, "new"); + sz_signal_list_set(items, sz_list_nil()); + name = sz_string_from_cstr("draft"); + got = sz_property_signal_str(name); + assert(strcmp(sz_string_cstr(got), "new") == 0); + sz_string_free(got); + sz_timeline_replay_from(0); + got = sz_property_signal_str(name); + assert(strcmp(sz_string_cstr(got), "old") == 0); + sz_string_free(got); + sz_release(name); + name = sz_string_from_cstr("items"); + assert(sz_property_signal_list_len(name) == 2); + got = sz_property_signal_list_at(name, 1); + assert(strcmp(sz_string_cstr(got), "b") == 0); + sz_string_free(got); + sz_timeline_replay_from(-1); + assert(sz_property_signal_list_len(name) == 0); + sz_release(name); + sz_property_session_reset(); + unsetenv("SCUZZ_TESTRT"); + sz_signal_str_free(draft); + sz_signal_list_free(items); +} + static void test_signal_list_spine_collect(void) { SzSignalList *items; SzList *xs; @@ -14428,6 +14696,13 @@ static void test_session_load_code(void) { sz_ui_session_take_root(session); sz_ui_session_set_rebuild(session, NULL, count); assert(sz_ui_session_load_code(session, RELOAD_A)); + { + char staged[128]; + FILE *st; + snprintf(staged, sizeof staged, "%s.load-1", RELOAD_A); + st = fopen(staged, "rb"); + assert(st == NULL); + } assert(sz_ui_session_reload(session)); a11y = sz_view_a11y_dump(sz_ui_session_root(session)); assert(strstr(sz_string_cstr(a11y), "text:A") != NULL); @@ -14541,6 +14816,7 @@ int main(void) { test_record_live_scroll(); test_studio_shaped_xy(); test_session_inject_script(); + test_session_inject_grows_past_4k(); test_session_inject_control(); test_session_dump_now_needs_path(); test_session_inject_scroll(); @@ -14549,6 +14825,7 @@ int main(void) { test_session_inject_key(); test_session_inject_key_utf8_backspace(); test_record_live_key(); + test_record_type_escapes(); test_session_inject_key_repeat(); test_session_inject_compose(); test_record_live_hover_secondary(); @@ -15035,6 +15312,7 @@ int main(void) { test_slider_paint_fill(); test_slider_in_taps_dump(); test_slider_pointer_drag(); + test_replace_root_drops_pointer(); test_slider_live_records_xy(); test_progress_sizes(); test_progress_unbounded_width(); @@ -15060,8 +15338,10 @@ int main(void) { test_button_does_not_wrap(); test_text_blank_line_from_newline(); test_a11y(); + test_a11y_dump_grows_past_4k(); test_clear_children(); test_view_each(); + test_view_each_setlist_rebuilds(); test_view_each_map_text(); test_view_each_map_button(); test_each_expanded_row_in_scroll(); @@ -15071,6 +15351,9 @@ int main(void) { test_property_signal_int(); test_signal_list_record_dump(); test_property_signal_str(); + test_signal_dump_escapes(); + test_signal_name_last_wins(); + test_property_replay_str_list(); test_text_field_edit(); test_view_editor(); test_view_editor_viewport(); diff --git a/docs/gaps.md b/docs/gaps.md index 7ac1d9c1..07128fe2 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -35,9 +35,9 @@ These gaps keep the distinctive claims kernel-shaped. Close them in this order. 1. **`Signal[T]` and `View.each` over records** — In: `Signal.list` holds `List[T]` over records and enums; `View.each` binds the element type; studio keeps tasks as `List[Item]`; a record list dumps `list[N] name = `. Open: one generic cell — `Signal.map` stays `Int => String` and there is no non-list `Signal[T]` cell. Direction: one generic cell and `View.each` over the element type. -2. **UTF-8 `String`** — In: `Str.*` indexes code points; `Str.byteLen` / `Str.byteSlice` keep bytes for framing; the kernel `utf8Ops` drive oracle proves multibyte ops. Case maps stay ASCII by design. Caret offsets in TextField/editor stay bytes. The editor and toolchain LSP framing uses `Str.byteLen` / `Str.byteSlice`. LLVM `[N x i8]` string sizing uses `Str.byteLen`. +2. **UTF-8 `String`** — In: `Str.*` indexes code points; an isolated continuation byte counts as one code point; `Str.byteLen` / `Str.byteSlice` keep bytes for framing; pad cycles complete code points; the `Str` kit is closed; the kernel `utf8Ops` drive oracle proves multibyte ops. Case maps stay ASCII by design. Caret offsets in TextField/editor stay bytes. The editor and toolchain LSP framing uses `Str.byteLen` / `Str.byteSlice`. LLVM `[N x i8]` string sizing uses `Str.byteLen`. -3. **Scuzz spans on panic and LSP** — In: `check` JSON diagnostics use recorded file stem, line, and column. No substring search. No hardcoded file. Product `scuzz lsp` is a stdio JSON-RPC server. It wraps `check`. JSON diagnostics stay the single schema. Goto-def uses `Fun.off`. Rename replaces lexer ident tokens. Hover names the ident under the caret. Completion filters by the caret prefix. Semantic tokens come from the lexer. Panic prints `scuzz panic: Main.scuzz:2:14: ` from that def's file, line, and column. The dogfood IDE consumes that schema. It does not replace it. +3. **Scuzz spans on panic and LSP** — In: `check` JSON diagnostics use recorded file stem, line, and column. No substring search. No hardcoded file. Product `scuzz lsp` is a stdio JSON-RPC server. It wraps `check`. JSON diagnostics stay the single schema. Overlay presence is a list entry. didChange reads full-sync `contentChanges`. Rename returns a WorkspaceEdit. Diagnostics run the check file list with overlays. Goto-def uses `Fun.off`. Rename replaces lexer ident tokens. Hover names the ident under the caret. Completion filters by the caret prefix from kit names and local defs. Semantic tokens come from the lexer. Panic prints `scuzz panic: Main.scuzz:2:14: ` from that def's file, line, and column. The dogfood IDE consumes that schema. It does not replace it. 4. **Typed fail `E`** — In: check encodes `IO[E, A]`. `IO[A]` means `IO[String, A]`. `IO.fail(e)` takes `E`. `handleErrorWith` binds `E`. `flatMap` keeps one `E`. Kits still fail with `String`. Open: the C `SzError` wire is still a string. Do not add `ZIO[R, E, A]`. Do not add user `IO.delay`. @@ -62,7 +62,7 @@ Do not start these before thesis-critical gaps close. - **Stable inject keys** — `tap N` / `scroll N` follow a11y preorder. A refactor can miss a stored corpus entry. Named control keys for inject stay after named claim observations. - **Simulation world** — TestRuntime seals the wire (no live sockets; Nth Fs / Net / Queue fault; PCT on fibers; Clock and Fs fakes). Clock skew, partitions, and a model to relate against stay later. -- **Mutation depth** — Mutation flips ops, swaps `if` arms, and replaces an ADT construct with a sibling. Inert mutants stay unreported. Semantic mutants stay later. +- **Mutation depth** — Mutation flips ops, swaps `if` arms, swaps tap handler bodies, and walks impl methods. Inert mutants stay unreported. Semantic mutants stay later. - **Memory** — Last-use retain/release is locked in [`vision.md`](vision.md) GC. Values with no last-use stay allocated until panic sweep or process exit. No cycle collector. - **Dependency forms beyond `path`** — Path deps only (`Manifest.scuzz`). Git, versioned, and hosted artifacts are direction. There is no registry. Revisit after path deps and file-as-module stay the reuse story. A lockfile identity can land before a registry. - **Windows desktop embedder** — same session protocol as X11/Cocoa. Secondary platform. diff --git a/docs/guide.md b/docs/guide.md index 32174f7a..a504147f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -19,13 +19,13 @@ scuzz ide --headless . # bundled editor; Desktop without --headless scuzz fmt # rewrite src/ (check already verifies format) ``` -Default `[ui]` link uses the pinned Skia CPU prebuilt (`third_party/skia/PIN`). Checkout builds fetch it on first `ffi-skia` make. Opt out with `SCUZZ_SKIA=sk_sw`. `SCUZZ_SKIA=gpu` presents through OpenGL. Missing OpenGL fails with one install line. +Default `[ui]` link uses the pinned Skia CPU prebuilt (`third_party/skia/PIN`). Checkout builds fetch it on first `ffi-skia` make and compile the in-tree shim into that archive. Opt out with `SCUZZ_SKIA=sk_sw`. `SCUZZ_SKIA=gpu` presents through OpenGL. Missing OpenGL fails with one install line. From a prebuilt tarball (no checkout build): `RELEASE_TGZ=scuzz-.tar.gz ./scripts/install.sh`. Produce one with `./scripts/package_release.sh`. That script compiles `examples/cli` with the newest GitHub `v*` bootstrap. Override with `SCUZZ_BOOTSTRAP_TAG` or `SCUZZ_BOOTSTRAP`. `install.sh --help` lists flags and the `curl | sh` invocation. `scuzz new --ui` scaffolds `scuzz.toml` with `[ui]`, a Counter-shaped `src/Main.scuzz`, and Headless-friendly defaults. -`scuzz ide [path]` launches the bundled `[ui]` editor (`examples/editor` in a checkout; `SCUZZ_HOME/ide` from a release). Desktop is the default. `--headless` stays a peer. Pass a file or a project directory. A directory lists as the tree root and opens `src/Main.scuzz` when that file exists, else the first `.scuzz` file in `src/` or in the directory. A dir tap expands or collapses children in place. Nested rows indent. The toolbar wraps. Tabs list each open file. Save all writes dirty tabs. Run and Fuzz append captured text to the output list. Palette lists Save, Check, Find, Complete, Hover, Format, Def, Rename, Fix, Save all, Run, and Fuzz. A tap on a tree or diagnostic row focuses that list. ArrowUp / ArrowDown move among sibling rows when no overlay is open. Enter / Space activate. There is no `scuzz-ide` binary. +`scuzz ide [path]` launches the bundled `[ui]` editor (`SCUZZ_IDE` if set, else `SCUZZ_HOME/ide` from a release, else `examples/editor` in a checkout). Desktop is the default. `--headless` stays a peer. Pass a file or a project directory. A directory lists as the tree root and opens `src/Main.scuzz` when that file exists, else the first `.scuzz` file in `src/` or in the directory. A dir tap expands or collapses children in place. Nested rows indent. The toolbar wraps. Tabs list each open file. Save all writes dirty tabs. Run and Fuzz append captured text to the output list. Palette lists Save, Check, Find, Complete, Hover, Format, Def, Rename, Fix, Save all, Run, and Fuzz. A tap on a tree or diagnostic row focuses that list. ArrowUp / ArrowDown move among sibling rows when no overlay is open. Enter / Space activate. There is no `scuzz-ide` binary. `scuzz package --target host` writes a Mobile host shell under `build/package/host`. `scuzz package --target android` packs a debug APK (needs the NDK and the Android SDK) into `build/package/android/`. When adb lists a device, the same command installs the APK. A USB serial wins over an emulator. No device is not a failure. Missing NDK or SDK fails with one install line. `scuzz package --target ios` builds a signed iOS simulator `.app` (needs Xcode) into `build/package/ios/`. Missing Xcode fails with one install line. A package that calls Net fails: mobile shells do not link OpenSSL. `[ui].bundle_id` is the Android package and iOS bundle id. The iOS and Android shells send typed text to TextField. SurfaceView taps on Android enter the same pump as iOS touches. @@ -38,23 +38,23 @@ scuzz test # compile + SCUZZ_TESTRT=1 exit-0 smoke scuzz run ``` -Console kit: `Sys.args(): IO[List[String]]`, `Sys.readLine(): IO[String]` (EOF → `""`; live parks on poll), `Sys.read(n): IO[String]` (n stdin bytes; fewer at EOF), `Sys.write(s): IO[Unit]` (stdout, no newline), `Sys.exec(cmd): IO[(Int, String, String)]` (exit code plus captured stdout and stderr; live parks on poll; fails under TestRuntime), `Sys.spawn(cmd): IO[Int]` (pid plus stdin/stdout pipes; fails under TestRuntime), `Sys.childWrite(pid, s): IO[Unit]`, `Sys.childRead(pid, n): IO[String]` (n child-stdout bytes; fewer at EOF; parks on poll), `Sys.childClose(pid): IO[Unit]` (close child stdin), `Sys.alive(pid): IO[Int]`, `Sys.kill(pid): IO[Unit]` (SIGTERM live; fake table under TestRuntime), `IO.println`. Watch files with `IO.sleep` / `Clock.monotonic` plus `Fs.read` / `Fs.exists` / `Fs.list`. There is no `Fs.watch`. TestRuntime fakes Clock and Fs so the same poll loop runs under Headless. Under `SCUZZ_TESTRT=1`, TestRuntime scripts stdin (`SCUZZ_TESTRT_STDIN` or `sz_testrt_stdin_feed`), optionally overrides argv, and captures println (still echoes to live stdout). `Sys.write` captures and does not write live stdout. `Sys.getenv` reads a sealed map (empty unless `SCUZZ_TESTRT_ENV` injects keys). `Sys.alive` / `Sys.kill` use a fake process table (no host `waitpid` / `kill`). Net uses stubs, injected serve paths, and a virtual loopback mailbox (no live sockets). See `examples/io` and `examples/hello`. +Console kit: `Sys.args(): IO[List[String]]`, `Sys.readLine(): IO[String]` (EOF → `""`; live parks on poll), `Sys.read(n): IO[String]` (n stdin bytes; fewer at EOF), `Sys.write(s): IO[Unit]` (stdout, no newline), `Sys.exec(cmd): IO[(Int, String, String)]` (exit code plus captured stdout and stderr; live parks on poll; 1 MiB cap fails; cancel reaps the child; fails under TestRuntime), `Sys.spawn(cmd): IO[Int]` (pid plus stdin/stdout pipes; stderr stays the parent stderr; fails under TestRuntime), `Sys.childWrite(pid, s): IO[Unit]`, `Sys.childRead(pid, n): IO[String]` (n child-stdout bytes; fewer at EOF; unknown pid fails; parks on poll), `Sys.childClose(pid): IO[Unit]` (close child stdin), `Sys.alive(pid): IO[Int]`, `Sys.kill(pid): IO[Unit]` (SIGTERM live; waitpid WNOHANG; slot stays until reaped; fake table under TestRuntime), `IO.println`. Watch files with `IO.sleep` / `Clock.monotonic` plus `Fs.read` / `Fs.exists` / `Fs.list`. There is no `Fs.watch`. `Fs.delete` removes a file or a directory tree. It refuses `/` and `.` after canonicalization. TestRuntime fakes Clock and Fs so the same poll loop runs under Headless. Under `SCUZZ_TESTRT=1`, TestRuntime scripts stdin (`SCUZZ_TESTRT_STDIN` or `sz_testrt_stdin_feed`), optionally overrides argv, and captures println (still echoes to live stdout). `Sys.write` captures and does not write live stdout. `Sys.getenv` reads a sealed map (empty unless `SCUZZ_TESTRT_ENV` injects keys). `Sys.alive` / `Sys.kill` use a fake process table (no host `waitpid` / `kill`). Net uses stubs, injected serve paths, and a virtual loopback mailbox (no live sockets). See `examples/io` and `examples/hello`. ## Kernel language (what you write) - `@main def main: IO[Unit] = …` entry; top-level `def` helpers - **`for { x = e; y <- io; if pred } yield r`** as the primary binder (pure `=`, effect `<-`). `(a, b) = e` / `(a, b, c) = e` and `(a, b) <- e` unpack a tuple of 2 through 8 slots. `Point(x, y) = p`, `Opt.Some(n) <- e`, and `h :: t = xs` unpack the same way. A miss panics. `if pred` keeps the rest when `pred` is true (`pred` is Bool). A miss is `IO.fail`. The `for` needs a `<-` binder. Nested `for` in `if` / lambda arms when multi-bind is needed. - No `val` / statement blocks. `{ case … }` is a lambda, not a block. -- Literals: ints, floats (`1.5`, `1.5e-3`, `1e10`), hex (`0xFF`), binary (`0b1010`), digit separators (`1_000`, `0xFF_00`), `true` / `false`, strings, triple-quoted strings (`"""…"""` / `s"""…$x…"""`), `()`, `s"…$x…"`, list literals `[a, b]`, cons `h :: t` (`List.cons`), tuple of 2 through 8 slots `(a, b)` -- Types: `Unit`, `Int`, `Float`, `String`, `Bool`, `Builder`, `List[T]`, `Option[T]`, `Map[K, V]`, `Set[T]`, `(A, B, …)`, `IO[T]`, `IO[E, A]`, `A => B`, `Fiber[A]` / `Ref[A]` / `Queue[A]` / `Deferred[A]` / `Resource[A]` / `Stream[A]`, nominal enums. `true` / `false` are `Bool`. `Bool` is not `Int`. `if` needs `Bool`. An `if` may omit `else` when the then arm is `Unit` or `IO[Unit]` (`if (ok) IO.println("y")`). Comparisons and `&&` / `||` return `Bool`. `==` / `!=` on String, List, Map, Set, tuples, enums, and records compare by value. Blessed handles compare by identity. Unary `!` is Bool. Unary `-` is Int or Float. Unary `~` and bitwise `&` / `|` / `^` / `<<` / `>>` are Int. Hex (`0xFF`) and binary (`0b1010`) are Int literals. Underscore may separate digits (`1_000`, `0xFF_00`). Scientific notation is a Float (`1.5e-3`, `1e10`). Triple-quoted strings (`"""…"""` / `s"""…$x…"""`) keep newlines. Identifiers may start with `_` (`_n`). Lone `_` discards. Call arguments may use `name = expr` (`add(m = 2, n = 1)`, `Point(y = 4, x = 3)`). Positionals come first. Named arguments fill remaining parameters by name. A `def` parameter may have a default (`add(n: Int, m: Int = 1)`, `greet(name: String, punct: String = "!")`). Omitted trailing arguments use the default. Named calls may omit any parameter that has a default. Defaults are closed expressions (no parameter names). Properties, traits, and methods do not take defaults. A call may end with a comma. An expression may pin its type with `e: T` (`Opt.None: Opt[Int]`, `[]: List[Int]`, `1 + 2: Int`). The checker uses `T` as the expected type. A typed position may construct with a bare uppercase case name (`None: Option[Int]`, `Some(1)`, `if (ok) Some(n) else None`). A lambda may pin its parameter with `(x: T) =>` (`List.map(xs, (n: Int) => Str.fromInt(n))`). The annotation must match the expected kit or `A => B` parameter type. A lambda may unpack a tuple of 2 through 8 slots with `(a, b) =>`. A kit or `A => B` argument may use one `_` hole (`List.map(xs, _ + 1)`, `List.map(xs, Str.fromInt(_))`, `List.filter(xs, _ != "b")`). `_ + _` is rejected. `_ =>` still discards. A kit or `A => B` argument may be a case lambda (`{ case Opt.Some(n) => n case Opt.None => 0 }`, `{ case Some(n) => n case None => 0 }`, `io.map({ case 0 => "z" case _ => "n" })`). It matches the bound value. Exhaustiveness is the same as match. A kit or `A => B` argument may be a unary def (`List.map(xs, Str.fromInt)`, `apply(id, 3)`). A def with 2 through 8 params eta-expands as `(A, B, …) => R` (`List.foldLeft(xs, 0, add)`, `applyPair(add, 2, 3)`). A `for` binder or def may name an `A => B` value (`inc = (_ + 1): Int => Int`, `typed = (n: Int) => n + 1`, `def addN(n: Int): Int => Int = (m: Int) => n + m`). `f(x)` applies that value. `f(x, y)` applies `(A, B) => C`. An `A => B` expression applies with `e(x)` on one line (`plusOne()(5)`, `addN(3)(4)`, `((n: Int) => n + 1)(6)`). An `(A, B) => C` expression applies with `e(x, y)` (`((a, b) => a + b)(2, 3)`). Pass a named Fun to a kit or `A => B` parameter (`List.map(xs, eta)`, `apply(inc, 5)`, `apply(plusOne(), 6)`). `{ … }` without `case` is illegal. `Option[T]` is `None` or `Some(x)` (`enum Option[T]: None | Some(x: T)`). `attempt` returns `IO[Result[A]]` (`enum Result[T]: Err(msg: String) | Ok(value: T)`). `handleErrorWith(err => …)` binds `err` as `String`. `IO.fail(e)` takes `E`. `IO[A]` means `IO[String, A]`. An `if` or match arm may be `IO.fail` next to another `IO[T]`. `io.map(f)` maps a success (`IO.pure(1).map(n => n + 1)`). The body is pure `B`. The result is `IO[B]`. `Float.fromInt(n)` and `Float.toInt(x)` convert (truncate toward zero). `s"$x"` interpolates `String`, `Int`, and `Float`. `Map.set` / `Map.get` / `Map.getOrElse` / `Map.contains` / `Map.remove` / `Map.keys` / `Map.values` / `Map.size` / `Map.isEmpty` / `Map.nonEmpty` / `Map.union` / `Map.intersect` / `Map.diff` / `Map.filter` / `Map.mapValues` / `Map.exists` / `Map.forall` and `Set.add` / `Set.contains` / `Set.remove` / `Set.toList` / `Set.size` / `Set.isEmpty` / `Set.nonEmpty` / `Set.union` / `Set.intersect` / `Set.diff` / `Set.isSubset` / `Set.isDisjoint` / `Set.filter` / `Set.map` / `Set.exists` / `Set.forall` are persistent (keys `Int` or `String`). `Builder.empty` / `Builder.append` / `Builder.result` assemble a string in linear time (`append` is pure copy-on-write). `Oracle.sumTo(n)` is a closed-form Int (`n*(n+1)/2` for `n >= 0`). A drive oracle may compare a Scuzz loop to that kit. The compare stays in-process. It does not use `Sys.exec`. `Map.get` is a list of the value, or empty. `Set.union` keeps keys from both sets. `Set.intersect` keeps keys in both. `Set.diff` keeps keys in the first set that are missing from the second. `Set.isSubset(a, b)` is true when every key of `a` is in `b`. Empty `a` is a subset. `Set.isDisjoint(a, b)` is true when `a` and `b` share no key. Empty is disjoint. `Map.union` keeps keys from both maps and takes values from the second map. `Map.intersect` keeps keys in both and takes values from the first map. `Map.diff` keeps keys in the first map that are missing from the second. `Map.filter(m, pred)` keeps entries whose value matches `pred`. Empty stays empty. `Map.mapValues(m, f)` maps each value. `Map.exists` / `Map.forall` test values (empty exists is false; empty forall is true). `Set.filter(s, pred)` keeps keys that match `pred`. `Set.map(s, f)` maps each key (`Int` or `String`; duplicates collapse). `Set.exists` / `Set.forall` test keys. -- Enums + **`record Name(f1: T1, …)`** (construct `Name(…)`, match `case Name(…)`, field `p.x`, update `p.copy(y = 9)` — see `examples/kernel`). `.copy` rebuilds the record. Named overrides select fields. Positionals fill fields in order. Omitted fields keep the receiver values. Matching an enum or record must cover every case or include `_`. `case Pat if pred =>` keeps the arm only when `pred` is true. `pred` is `Bool`. A guarded arm does not cover the pattern. Match may use a literal pattern (`case 0 =>`, `case "ok" =>`, `case true =>`). `true` and `false` together cover Bool. Int, Float, and String literals do not cover the type. Include `_` or a name bind. Match may use an or-pattern (`case Color.Red | Color.Blue =>`). Nested or in a payload works (`case Opt.Some(0 | 1)`). The body must typecheck for every alternative. Match may use an as-pattern (`case n @ Opt.Some(_)`). `n` binds the whole value. Nested as in a payload works (`case Opt.Some(n @ 0)`). `n @ A | B` binds `n` for every alternative. Match may use a list pattern (`case [] =>`, `case x :: xs =>`, `case [a, b] =>`). `[]` and `_ :: _` together cover `List`. Match may use a tuple of 2 through 8 slots pattern (`case (a, b)`). `(A, B, …)` is exhaustive. `p._1` … `p._N` project slots. A `for` binder and a lambda may unpack (`(a, b) = e`, `(a, b) <- e`, `(a, b) =>`, `Point(x, y) = p`, `Opt.Some(n) <- e`, `h :: t = xs`, `(Opt.Some(n)) =>`). Nested tuples are allowed. A miss panics. `IO.both` is `IO[(A, B)]`. `IO.race` needs matching `IO[T]` arms. `IO.fail` may fill one arm. Match may use a named field pattern (`case Point(x = n)`, `case Opt.Some(x = n)`). Omitted fields are `_`. Positionals come first. A named field after a positional is allowed (`case Point(n, y = 0)`). Match may use a bare constructor name that starts with an uppercase letter (`case None`, `case Some(n)`, `case Red | Blue`). The scrutinee type selects the case. A lowercase name still binds. `check` reports `non-exhaustive match: missing Color.Blue`. Nested payload patterns work (`case Wrap.Box(Color.Red)` / `case Opt.Some(0)`). A specialized nested case without a catch-all is non-exhaustive (`missing Wrap.Box(Color.Blue)`). +- Literals: ints, floats (`1.5`, `1.5e-3`, `1e10`), hex (`0xFF`), binary (`0b1010`), digit separators (`1_000`, `0xFF_00`), `true` / `false`, strings, triple-quoted strings (`"""…"""`), `()`, `s"…$x…"`, list literals `[a, b]`, cons `h :: t` (`List.cons`), tuple of 2 through 8 slots `(a, b)` +- Types: `Unit`, `Int`, `Float`, `String`, `Bool`, `Builder`, `List[T]`, `Option[T]`, `Map[K, V]`, `Set[T]`, `(A, B, …)`, `IO[T]`, `IO[E, A]`, `A => B`, `Fiber[A]` / `Ref[A]` / `Queue[A]` / `Deferred[A]` / `Resource[A]` / `Stream[A]`, nominal enums. `true` / `false` are `Bool`. `Bool` is not `Int`. `if` needs `Bool`. An `if` may omit `else` when the then arm is `Unit` or `IO[Unit]` (`if (ok) IO.println("y")`). Comparisons and `&&` / `||` return `Bool`. `==` / `!=` on String, List, Map, Set, tuples, enums, and records compare by value. Blessed handles compare by identity. Unary `!` is Bool. Unary `-` is Int or Float. Unary `~` and bitwise `&` / `|` / `^` / `<<` / `>>` are Int. Hex (`0xFF`) and binary (`0b1010`) are Int literals. Underscore may separate digits (`1_000`, `0xFF_00`). Scientific notation is a Float (`1.5e-3`, `1e10`). Triple-quoted strings (`"""…"""`) keep newlines. Use `s"…$x…"` for interpolation. The dialect has no line comments. Identifiers may start with `_` (`_n`). Lone `_` discards. Call arguments may use `name = expr` (`add(m = 2, n = 1)`, `Point(y = 4, x = 3)`). Positionals come first. Named arguments fill remaining parameters by name. A `def` parameter may have a default (`add(n: Int, m: Int = 1)`, `greet(name: String, punct: String = "!")`). Omitted trailing arguments use the default. Named calls may omit any parameter that has a default. Defaults are closed expressions (no parameter names). Properties, traits, and methods do not take defaults. A call may end with a comma. An expression may pin its type with `e: T` (`Opt.None: Opt[Int]`, `[]: List[Int]`, `1 + 2: Int`). The checker uses `T` as the expected type. A typed position may construct with a bare uppercase case name (`None: Option[Int]`, `Some(1)`, `if (ok) Some(n) else None`). A lambda may pin its parameter with `(x: T) =>` (`List.map(xs, (n: Int) => Str.fromInt(n))`). The annotation must match the expected kit or `A => B` parameter type. A lambda may unpack a tuple of 2 through 8 slots with `(a, b) =>`. A kit or `A => B` argument may use one `_` hole (`List.map(xs, _ + 1)`, `List.map(xs, Str.fromInt(_))`, `List.filter(xs, _ != "b")`). `_ + _` is rejected. `_ =>` still discards. A kit or `A => B` argument may be a case lambda (`{ case Opt.Some(n) => n case Opt.None => 0 }`, `{ case Some(n) => n case None => 0 }`, `io.map({ case 0 => "z" case _ => "n" })`). It matches the bound value. Exhaustiveness is the same as match. A kit or `A => B` argument may be a unary def (`List.map(xs, Str.fromInt)`, `apply(id, 3)`). A def with 2 through 8 params eta-expands as `(A, B, …) => R` (`List.foldLeft(xs, 0, add)`, `applyPair(add, 2, 3)`). A `for` binder or def may name an `A => B` value (`inc = (_ + 1): Int => Int`, `typed = (n: Int) => n + 1`, `def addN(n: Int): Int => Int = (m: Int) => n + m`). `f(x)` applies that value. `f(x, y)` applies `(A, B) => C`. An `A => B` expression applies with `e(x)` on one line (`plusOne()(5)`, `addN(3)(4)`, `((n: Int) => n + 1)(6)`). An `(A, B) => C` expression applies with `e(x, y)` (`((a, b) => a + b)(2, 3)`). Pass a named Fun to a kit or `A => B` parameter (`List.map(xs, eta)`, `apply(inc, 5)`, `apply(plusOne(), 6)`). `{ … }` without `case` is illegal. `Option[T]` is `None` or `Some(x)` (`enum Option[T]: None | Some(x: T)`). `attempt` returns `IO[Result[A]]` (`enum Result[T]: Err(msg: String) | Ok(value: T)`). `handleErrorWith(err => …)` binds `err` as `String`. `IO.fail(e)` takes `E`. `IO[A]` means `IO[String, A]`. An `if` or match arm may be `IO.fail` next to another `IO[T]`. `io.map(f)` maps a success (`IO.pure(1).map(n => n + 1)`). The body is pure `B`. The result is `IO[B]`. `Float.fromInt(n)` and `Float.toInt(x)` convert (truncate toward zero). `s"$x"` interpolates `String`, `Int`, and `Float`. `Map.set` / `Map.get` / `Map.getOrElse` / `Map.contains` / `Map.remove` / `Map.keys` / `Map.values` / `Map.size` / `Map.isEmpty` / `Map.nonEmpty` / `Map.union` / `Map.intersect` / `Map.diff` / `Map.filter` / `Map.mapValues` / `Map.exists` / `Map.forall` and `Set.add` / `Set.contains` / `Set.remove` / `Set.toList` / `Set.size` / `Set.isEmpty` / `Set.nonEmpty` / `Set.union` / `Set.intersect` / `Set.diff` / `Set.isSubset` / `Set.isDisjoint` / `Set.filter` / `Set.map` / `Set.exists` / `Set.forall` are persistent (keys `Int` or `String`). `Builder.empty` / `Builder.append` / `Builder.result` assemble a string in linear time (`append` is pure copy-on-write). `Oracle.sumTo(n)` is a closed-form Int (`n*(n+1)/2` for `n >= 0`). A drive oracle may compare a Scuzz loop to that kit. The compare stays in-process. It does not use `Sys.exec`. `Map.get` is `Option[V]`. A miss is `None`. A stored empty list is `Some`. `Set.union` keeps keys from both sets. `Set.intersect` keeps keys in both. `Set.diff` keeps keys in the first set that are missing from the second. `Set.isSubset(a, b)` is true when every key of `a` is in `b`. Empty `a` is a subset. `Set.isDisjoint(a, b)` is true when `a` and `b` share no key. Empty is disjoint. `Map.union` keeps keys from both maps and takes values from the second map. `Map.intersect` keeps keys in both and takes values from the first map. `Map.diff` keeps keys in the first map that are missing from the second. `Map.filter(m, pred)` keeps entries whose value matches `pred`. Empty stays empty. `Map.mapValues(m, f)` maps each value. `Map.exists` / `Map.forall` test values (empty exists is false; empty forall is true). `Set.filter(s, pred)` keeps keys that match `pred`. `Set.map(s, f)` maps each key (`Int` or `String`; duplicates collapse). The output key kind comes from the mapped key. `Set.exists` / `Set.forall` test keys. +- Enums + **`record Name(f1: T1, …)`** (construct `Name(…)`, match `case Name(…)`, field `p.x`, update `p.copy(y = 9)` — see `examples/kernel`). `.copy` rebuilds the record. Named overrides select fields. Positionals fill fields in order. Omitted fields keep the receiver values. Matching an enum or record must cover every case or include `_`. `case Pat if pred =>` keeps the arm only when `pred` is true. `pred` is `Bool`. A guarded arm does not cover the pattern. Match may use a literal pattern (`case 0 =>`, `case "ok" =>`, `case true =>`). `true` and `false` together cover Bool. Int, Float, and String literals do not cover the type. Include `_` or a name bind. Match may use an or-pattern (`case Color.Red | Color.Blue =>`). Nested or in a payload works (`case Opt.Some(0 | 1)`). The body must typecheck for every alternative. Match may use an as-pattern (`case n @ Opt.Some(_)`). `n` binds the whole value. Nested as in a payload works (`case Opt.Some(n @ 0)`). `n @ A | B` binds `n` for every alternative. Match may use a list pattern (`case [] =>`, `case x :: xs =>`, `case [a, b] =>`). `[]` and `_ :: _` together cover `List`. Match may use a tuple of 2 through 8 slots pattern (`case (a, b)`). `(A, B, …)` is exhaustive. `e._1` … `e._N` project slots of any tuple expression. A `for` binder and a lambda may unpack (`(a, b) = e`, `(a, b) <- e`, `(a, b) =>`, `Point(x, y) = p`, `Opt.Some(n) <- e`, `h :: t = xs`, `(Opt.Some(n)) =>`). Nested tuples are allowed. A miss panics. `IO.both` is `IO[(A, B)]`. `IO.race` needs matching `IO[T]` arms. `IO.fail` may fill one arm. Match may use a named field pattern (`case Point(x = n)`, `case Opt.Some(x = n)`). Omitted fields are `_`. Positionals come first. A named field after a positional is allowed (`case Point(n, y = 0)`). Match may use a bare constructor name that starts with an uppercase letter (`case None`, `case Some(n)`, `case Red | Blue`). The scrutinee type selects the case. A lowercase name still binds. `check` reports `non-exhaustive match: missing Color.Blue`. Nested payload patterns work (`case Wrap.Box(Color.Red)` / `case Opt.Some(0)`). A specialized nested case without a catch-all is non-exhaustive (`missing Wrap.Box(Color.Blue)`). - Thin **traits** / `impl` with static dispatch (`p.show()` / `p.getOrElse(0)` — including `impl Get[Int] for Point` and `impl Get[T] for Opt`; see `examples/kernel`) - Thin **generics**: `def id[T](x: T): T = x` monomorphized at call sites (`examples/kernel`); generic enums/records too — `enum Opt[T]:` with `o.getOrElse(0)` / `record Box[T](x: T): def get(): T = self.x` (type methods are indented `def`s after cases or after record `:`, same shape as `impl`). Instantiation inferred from ctor args, the expected type, or `e: T` (`examples/kernel`). - **Type aliases**: `type UserId = Int` / `type BoxList[T] = List[T]`. The checker expands the name in params, returns, and `e: T`. `import Module.UserId` binds the alias. -- Blessed impurity only: `IO.println` / `sleep` / `fail` / `pure` / `race` / `both` / `ensure` / `timeout` / `forever` / `repeatN` / `retryN` / `foreach` / `foreachDiscard` / `when` / `unless`, `.map` / `.flatMap` / `.handleErrorWith` / `.attempt`, `Fiber.fork` / `join` / `interrupt`, `Ref.*` / `Queue.*` / `Deferred.*` (`Ref.of` infers `A`; pin `Queue[Int]` / `Deferred[Int]` with `: IO[Queue[Int]]`; `Ref.update` / `Ref.updateAndGet`), `Resource.make` / `Resource.use` (`Resource[A]` from acquire `IO[A]`; release on success, failure, and cancel), `Stream.emit` / `emits` / `eval` / `concat` / `map` / `evalMap` / `evalTap` / `filter` / `filterNot` / `take` / `takeWhile` / `drop` / `dropWhile` / `find` / `findLast` / `exists` / `forall` / `none` / `range` / `repeatN` / `zip` / `zipWith` / `zipAll` / `zipWithIndex` / `interleave` / `intersperse` / `grouped` / `sliding` / `takeRight` / `dropRight` / `flatten` / `flatMap` / `mapConcat` / `scan` / `fold` / `changes` / `orElse` / `iterate` / `unfold` / `head` / `last` / `count` / `compileToList` / `drain` (`Stream[A]` from emit/emits/eval/range/iterate/unfold; `exists` / `forall` / `none` are `IO[Bool]`; `fold` is `IO[Z]`; `head` / `last` are `IO[A]` and fail when empty; `count` is `IO[Int]`; `zip` / `zipAll` are `Stream[(A, B)]`; `interleave` is `Stream[A]`; `grouped` / `sliding` are `Stream[List[A]]`), `Fs.*`, `Json.parse` / `Json.stringify` (enum `Json`: `Null|Bool|Int|Float|Str|Arr|Obj`; `parse` is `Result[Json]`; `stringify` is `Result[String]`; query: `get` / `keys` / `arr` / `at` / `has` / `pairs` / `is*` / `as*` / `*Or` / `getBool` / `getInt` / `getStr` / `getFloat` / `merge`; write: `set` / `remove` / `append` / `prepend` / `setAt` / `dropAt`; a miss is an empty list; `set` on a non-Obj is a one-key Obj; `append` / `prepend` on a non-Arr is a one-cell Arr), `Sys.args` / `Sys.readLine` / `Sys.read` / `Sys.write` / `Sys.exec` / `Sys.spawn` / `Sys.childWrite` / `Sys.childRead` / `Sys.childClose` / `Sys.alive` / `Sys.kill` / `Sys.getenv`, `Clock.*`, `Random.nextInt` (`IO[Int]` in `[0, bound)`), `Net.httpGet` / `Net.httpPost` / `Net.httpPut` / `Net.httpPatch` / `Net.httpDelete` / `Net.httpHead` / `Net.serveOnce` / `Net.serve` (handler receives `(path, method, body)` and returns the response body as `IO[String]`) / `Net.tcpConnect` / `Net.tcpListen` / `Net.tcpAccept` / `Net.tcpRead` / `Net.tcpWrite` / `Net.tcpClose` / `Net.udpBind` / `Net.udpSend` / `Net.udpRecv` / `Net.udpClose` +- Blessed impurity only: `IO.println` / `sleep` / `fail` / `pure` / `race` / `both` / `ensure` / `timeout` / `forever` / `repeatN` / `retryN` / `foreach` / `foreachDiscard` / `when` / `unless`, `.map` / `.flatMap` / `.handleErrorWith` / `.attempt`, `Fiber.fork` / `join` / `interrupt`, `Ref.*` / `Queue.*` (`Ref.of` infers `A`; pin `Queue[Int]` with `: IO[Queue[Int]]`; `Ref.update` / `Ref.updateAndGet`), `Deferred.empty` / `get` / `complete` / `fail` (`Deferred.get` returns `IO[A]` when the handle is `Deferred[A]`; pin with `: IO[Deferred[Int]]`; `fail` takes a `String` message), `Resource.make` / `Resource.use` (`Resource[A]` from acquire `IO[A]`; release on success, failure, and cancel), `Stream.emit` / `emits` / `eval` / `concat` / `map` / `evalMap` / `evalTap` / `filter` / `filterNot` / `take` / `takeWhile` / `drop` / `dropWhile` / `find` / `findLast` / `exists` / `forall` / `none` / `range` / `repeatN` / `zip` / `zipWith` / `zipAll` / `zipWithIndex` / `interleave` / `intersperse` / `grouped` / `sliding` / `takeRight` / `dropRight` / `flatten` / `flatMap` / `mapConcat` / `scan` / `fold` / `changes` / `orElse` / `iterate` / `unfold` / `head` / `last` / `count` / `compileToList` / `drain` (`Stream[A]` from emit/emits/eval/range/iterate/unfold; bind a Stream with `=`; `<-` needs IO; `take` pulls until n outputs; range / iterate / unfold allocate on pull; unfold cap is 65536 pulled steps; `exists` / `forall` / `none` are `IO[Bool]`; `fold` is `IO[Z]`; `head` / `last` are `IO[A]` and fail when empty; `count` is `IO[Int]`; `zip` / `zipAll` are `Stream[(A, B)]`; `interleave` is `Stream[A]`; `grouped` / `sliding` are `Stream[List[A]]`), `Fs.*`, `Json.parse` / `Json.stringify` (enum `Json`: `Null|Bool|Int|Float|Str|Arr|Obj`; `parse` is `Result[Json]`; `stringify` is `Result[String]`; query: `get` / `keys` / `arr` / `at` / `has` / `pairs` / `is*` / `as*` / `*Or` / `getBool` / `getInt` / `getStr` / `getFloat` / `merge`; write: `set` / `remove` / `append` / `prepend` / `setAt` / `dropAt`; a miss is an empty list; `set` on a non-Obj is a one-key Obj; `append` / `prepend` on a non-Arr is a one-cell Arr), `Sys.args` / `Sys.readLine` / `Sys.read` / `Sys.write` / `Sys.exec` / `Sys.spawn` / `Sys.childWrite` / `Sys.childRead` / `Sys.childClose` / `Sys.alive` / `Sys.kill` / `Sys.getenv`, `Clock.*`, `Random.nextInt` (`IO[Int]` in `[0, bound)`; bound <= 0 is `IO.fail`), `Net.httpGet` / `Net.httpPost` / `Net.httpPut` / `Net.httpPatch` / `Net.httpDelete` / `Net.httpHead` / `Net.serveOnce` / `Net.serve` (handler receives `(path, method, body)` and returns the response body as `IO[String]`) / `Net.tcpConnect` / `Net.tcpListen` / `Net.tcpAccept` / `Net.tcpRead` / `Net.tcpWrite` / `Net.tcpClose` / `Net.udpBind` / `Net.udpSend` / `Net.udpRecv` / `Net.udpClose` - No raw side effects in View build. Taps may run `IO` through `sz_io_unsafe_run`. The tap drops a leftover owned value or the run result. -The product CLI is Scuzz (`examples/cli`). `scuzz --help` and `scuzz --help` list flags and examples. `scuzz check` is the linter (format-verify + typecheck). `scuzz fmt` rewrites. `scuzz watch` rebuilds on source change. It does not reload a running process. `[ui]` `scuzz run --watch` is hot reload: it keeps the process and stamp-reloads the View tree (Signals stay). IO-only `scuzz run --watch` kills and reruns the process on source change. `[ui]` build emits `build/reload.dylib`. On source change, watch recompiles it then stamps so the session `dlopen`s new machine code (`SCUZZ_UI_RELOAD_CODE`). The process rewrites `build/debug.dump` (signal store + a11y including live `View.bindText` + `[taps]` with frames / `[fields]` plus `caret=B sel=A:C` (and `preedit=` when compose is set) / `[editor]` buffer plus `caret=B sel=A:C sx=X sy=Y lines=L` when a `View.editor` is present (plus `diag=` / `tok=` / `inlay=` / `fold=` / `preedit=` when set) / `[scrolls]` inject indices, same format as `scuzz test` goldens; `[last_hit]` after a TAP; `[hover]` after a no-button MOVE; `[last_secondary]` after a button-3 click; `[session]` kind/size/title/focus/lifecycle/pumps; `[splits]` / `[overlays]` when present; `[heap]` alloc stats with kind census, delta, and `[live]` remaining blocks) on dirty pumps so agents can read live UI state, `tap N` without guessing coordinates, and see which TextField `text` / `type` / `key` / `compose` / `caret` / `select` / `backspace` hit (`N* placeholder="live" caret=B sel=A:C`). A `View.editor` dumps under `[editor]` (`N* caret=B sel=A:C sx=X sy=Y lines=L "buffer"`). Newlines stay as `\n`. Diagnostic marks append `diag=P:S`. LSP span counts append `tok=N` / `inlay=N` / `fold=N` when non-zero. Compose appends `preedit="…"` when the preview is non-empty. Append `tap` / `xy` / `text` / `type` / `key` / `compose` / `commit` / `caret` / `select` / `copy` / `cut` / `paste` / `drag` / `hover` / `secondary` / `pump` / `scroll` / `backspace` / `dump` / `reload` / `quit` / `resetpeak` lines to `build/inject.script` to drive the session (rewrite plays the whole file). `dump` rewrites the debug dump now. `reload` rebuilds the View factory. `quit` stops the live session. Desktop quit is window close. `resetpeak` sets peak bytes to live and marks the heap delta. Panic prints remaining `[heap]` and `[live]`, writes `build/debug.dump.panic` when a live dump path is set, then frees remaining live blocks and abort. `text N s` / `type N s` / `backspace N k` / `caret N b` / `select N a c` / `scroll N dy` target dump index N. `key [+shift|+ctrl|+cmd|+alt|+repeat] [text]` uses the starred field or focused editor (`Enter`, `Backspace`, `ArrowLeft`, `a`). `+repeat` is a held-key auto-repeat (same insert / move / delete as a discrete key). `compose ` sets IME preedit (underlined preview; not in the committed buffer). `compose` with no text, or `commit`, inserts the preedit at the caret. `key Escape` cancels preedit. `caret ` sets the starred-field or focused-editor caret byte offset. `caret N b` targets dump index N. `select ` sets the starred-field selection. `copy` / `cut` / `paste` / `paste ` drive the session clipboard. Headless `paste` is first-class. Desktop/Mobile pull the OS pasteboard on paste when present. `drag x1 y1 x2 y2` is pointer-drag select. Live OS keys record as `key`, not `type`. Live OS auto-repeat records `key a+repeat`. Live OS copy/cut/paste and Shift+arrows record those verbs. One-token forms (`text s`, `backspace k`, `scroll 40`) still use the starred field or first Scroll. `text 0` remains payload `"0"`. `xy x y` injects a TAP at a logical point; a miss does not panic. `hover x y` injects a pointer MOVE with no button and shows `View.tooltip`. `secondary N` / `secondary x y` is a button-3 click; it does not fire the primary tap. Live OS hover and right-click record as `hover` / `secondary`. Desktop/Mobile `scuzz run` records live OS clicks and keys to `build/record.script` (not `inject.script`) and writes `build/debug.dump`. Replay with `scuzz run --headless --script build/record.script --dump build/debug.dump`. `--message-format=json` applies to `check` only. That JSON is the editor protocol. +The product CLI is Scuzz (`examples/cli`). `scuzz --help` and `scuzz --help` list flags and examples. `scuzz check` is the linter (format-verify + typecheck). `scuzz fmt` rewrites. `scuzz watch` rebuilds on source change. It does not reload a running process. `[ui]` `scuzz run --watch` is hot reload: it keeps the process and stamp-reloads the View tree (Signals stay). IO-only `scuzz run --watch` kills and reruns the process on source change. `[ui]` build emits `build/reload.dylib`. On source change, watch recompiles it then stamps so the session `dlopen`s new machine code (`SCUZZ_UI_RELOAD_CODE`). The process rewrites `build/debug.dump` (signal store + a11y including live `View.bindText` + `[taps]` with frames / `[fields]` plus `caret=B sel=A:C` (and `preedit=` when compose is set) / `[editor]` buffer plus `caret=B sel=A:C sx=X sy=Y lines=L` when a `View.editor` is present (plus `diag=` / `tok=` / `inlay=` / `fold=` / `preedit=` when set) / `[scrolls]` inject indices, same format as `scuzz test` goldens; `[last_hit]` after a TAP; `[hover]` after a no-button MOVE; `[last_secondary]` after a button-3 click; `[session]` kind/size/title/focus/lifecycle/pumps; `[splits]` / `[overlays]` when present; `[heap]` alloc stats with kind census, delta, and `[live]` remaining blocks) on dirty pumps so agents can read live UI state, `tap N` without guessing coordinates, and see which TextField `text` / `type` / `key` / `compose` / `caret` / `select` / `backspace` hit (`N* placeholder="live" caret=B sel=A:C`). A `View.editor` dumps under `[editor]` (`N* caret=B sel=A:C sx=X sy=Y lines=L "buffer"`). Newlines stay as `\n`. Signal dump strings and inject `type` / `paste` / `compose` payloads escape backslash, quote, newline, CR, and tab. Diagnostic marks append `diag=P:S`. LSP span counts append `tok=N` / `inlay=N` / `fold=N` when non-zero. Compose appends `preedit="…"` when the preview is non-empty. Append `tap` / `xy` / `text` / `type` / `key` / `compose` / `commit` / `caret` / `select` / `copy` / `cut` / `paste` / `drag` / `hover` / `secondary` / `pump` / `scroll` / `backspace` / `dump` / `reload` / `quit` / `resetpeak` lines to `build/inject.script` to drive the session (rewrite plays the whole file). `dump` rewrites the debug dump now. `reload` rebuilds the View factory. `quit` stops the live session. Desktop quit is window close. `resetpeak` sets peak bytes to live and marks the heap delta. Panic prints remaining `[heap]` and `[live]`, writes `build/debug.dump.panic` when a live dump path is set, then frees remaining live blocks and abort. `text N s` / `type N s` / `backspace N k` / `caret N b` / `select N a c` / `scroll N dy` target dump index N. `key [+shift|+ctrl|+cmd|+alt|+repeat] [text]` uses the starred field or focused editor (`Enter`, `Backspace`, `ArrowLeft`, `a`). `+repeat` is a held-key auto-repeat (same insert / move / delete as a discrete key). `compose ` sets IME preedit (underlined preview; not in the committed buffer). `compose` with no text, or `commit`, inserts the preedit at the caret. `key Escape` cancels preedit. `caret ` sets the starred-field or focused-editor caret byte offset. `caret N b` targets dump index N. `select ` sets the starred-field selection. `copy` / `cut` / `paste` / `paste ` drive the session clipboard. Headless `paste` is first-class. Desktop/Mobile pull the OS pasteboard on paste when present. `drag x1 y1 x2 y2` is pointer-drag select. Live OS keys record as `key`, not `type`. Live OS auto-repeat records `key a+repeat`. Live OS copy/cut/paste and Shift+arrows record those verbs. One-token forms (`text s`, `backspace k`, `scroll 40`) still use the starred field or first Scroll. `text 0` remains payload `"0"`. `xy x y` injects a TAP at a logical point; a miss does not panic. `hover x y` injects a pointer MOVE with no button and shows `View.tooltip`. `secondary N` / `secondary x y` is a button-3 click; it does not fire the primary tap. Live OS hover and right-click record as `hover` / `secondary`. Desktop/Mobile `scuzz run` records live OS clicks and keys to `build/record.script` (not `inject.script`) and writes `build/debug.dump`. Replay with `scuzz run --headless --script build/record.script --dump build/debug.dump`. `--message-format=json` applies to `check` only. That JSON is the editor protocol. ## View + Signal + Ui @@ -72,9 +72,9 @@ Build a pure `View` tree. Hold state in `Signal`. Run a session with `Ui.run`: } yield () ``` -Lists: keep a `Signal.list`, render with `View.each(items)` (framework rebuilds `- item` texts at layout). `View.each(items, s => view)` builds one child per element. The element type comes from the signal (`Signal.list(xs)` over `List[T]` binds `T`; record fields like `item.label` work in the body). String lists dump `["a", "b"]`; other element types dump the count as `list[N] = `. `List.filter(xs, pred)` keeps elements for which `pred` is true. `List.map(xs, f)` builds a new list. The filter/map/flatMap/find/findLast/exists/count/takeWhile/dropWhile/forall/filterNot/indexWhere/lastIndexWhere/span/partition/prefixLength/segmentLength/sortBy/maxBy/minBy/groupBy/distinctBy/`IO.foreach`/`IO.foreachDiscard` lambda binds the element type. `Ref.update` / `Ref.updateAndGet` bind the cell. `Map.filter` / `Map.exists` / `Map.forall` / `Map.mapValues` bind the value. `Set.filter` / `Set.exists` / `Set.forall` / `Set.map` bind the key. `List.tabulate(n, f)` binds `Int`. `List.setAt(xs, i, v)` replaces the element at `i`. An index outside the list leaves the list. `List.take(xs, n)` keeps the first `n` elements (`n` <= 0 is empty). `List.drop(xs, n)` skips `n` (`n` <= 0 leaves the list). `List.find(xs, pred)` is a list of the first match, or empty. `List.exists(xs, pred)` is true when any element matches. `List.takeWhile(xs, pred)` keeps a prefix while `pred` is true. `List.dropWhile(xs, pred)` skips that prefix. `List.forall(xs, pred)` is true when every element matches (empty is true). `List.filterNot(xs, pred)` keeps elements for which `pred` is false. `List.count(xs, pred)` is the number of matches. `List.flatMap(xs, f)` concatenates the lists that `f` returns. `List.padTo(xs, n, x)` appends `x` until length `n`. `n` <= len leaves the list. `List.nonEmpty(xs)` is true when the list has a cell. `List.empty()` is Nil. `List.len(xs)` is the cell count. `List.head(xs)` is `Option[T]`. Empty is `None`. `List.at(xs, i)` is the element at `i`. An index outside panics. `List.tail(xs)` drops the first cell. Empty panics. `List.join(xs, sep)` joins string cells. `List.concat(xs, ys)` copies the `xs` spine and shares `ys`. `List.flatten(xss)` concatenates inner lists. `List.takeRight(xs, n)` keeps the last `n` elements (`n` <= 0 is empty). `List.dropRight(xs, n)` drops the last `n` (`n` <= 0 leaves the list). `List.init(xs)` drops the last cell (empty stays empty). `List.last(xs)` is a list of the last element, or empty. `List.getOrElse(xs, i, default)` is the element at `i`, or `default` when `i` is out of range. `List.fill(n, x)` is `n` copies of `x` (`n` <= 0 is empty). `List.range(from, until)` is boxed ints `[from, until)` (empty when `until` <= `from`). `List.tabulate(n, f)` is `f(0)` … `f(n-1)` (`n` <= 0 is empty). `List.intersperse(xs, x)` inserts `x` between cells. Empty or one cell shares. `List.grouped(xs, n)` is chunks of length `n`. The last chunk may be short. `n` <= 0 is empty. `List.sliding(xs, n)` is overlapping windows of length `n`. `n` <= 0 or `n` > len is empty. `List.slice(xs, from, until)` is `[from, until)`. Negative `from` / `until` is 0. `until` <= `from` is empty. `List.indexWhere(xs, pred)` is the first matching index, or `-1`. `List.lastIndexWhere(xs, pred)` is the last matching index, or `-1`. `List.indices(xs)` is boxed ints `[0, len)`. Empty when `xs` is empty. `List.splitAt(xs, n)` is two lists: take then drop, packed as `List[List[T]]`. `List.span(xs, pred)` is takeWhile then dropWhile. `List.partition(xs, pred)` is filter then filterNot. `List.inits(xs)` is prefixes including empty and the full list. `List.tails(xs)` is suffixes including the full list and empty. `List.zip(xs, ys)` is `List[(A, B)]`. A and B may differ. It stops at the shorter list. `List.interleave(xs, ys)` alternates cells from `xs` and `ys`, then appends the leftover. Empty or one list shares. `List.zipAll(xs, ys, x, y)` pads the shorter list with `x` or `y`. `List.unzip(pairs)` is `(List[A], List[B])`. Empty unzip is two empty lists. `List.zipWithIndex(xs)` is `List[(Int, T)]`. `List.foldLeft(xs, z, f)` folds with `f(acc, x)` and returns `z` when `xs` is empty. `List.foldRight(xs, z, f)` folds with `f(x, acc)` from the right. `List.scanLeft(xs, z, f)` is the list of accumulators including `z`. Empty is `[z]`. `List.scanRight(xs, z, f)` scans from the right. `List.reduceLeft(xs, f)` folds from the first cell. Empty panics. `List.reduceRight(xs, f)` folds from the last cell. Empty panics. `List.transpose(xss)` turns rows into columns. It stops at the shortest row. Empty `xss` is empty. `List.contains(xs, x)` is true when a cell equals `x`. Strings and boxed ints compare by value. `List.indexOf(xs, x)` is the first matching index, or `-1`. `List.lastIndexOf(xs, x)` is the last matching index, or `-1`. `List.distinct(xs)` keeps the first cell of each equal value. `List.distinctBy(xs, f)` keeps the first cell of each `Int` or `String` key that `f` returns. Empty stays empty. `List.toMap(pairs)` is `Map[K, V]` from `List[(K, V)]`. Duplicate keys keep the last value. Empty is empty. `List.toSet(xs)` is `Set[T]` from `List[Int]` or `List[String]`. Duplicate cells collapse. Empty is empty. `Map.toList(m)` is `List[(K, V)]` in key order. Empty is empty. `List.diff(xs, ys)` keeps cells of `xs` that are missing from `ys`. `List.intersect(xs, ys)` keeps cells of `xs` that occur in `ys`. `List.startsWith(xs, prefix)` is true when `xs` begins with `prefix`. Empty prefix is true. `List.endsWith(xs, suffix)` is true when `xs` ends with `suffix`. Empty suffix is true. `List.sameElements(xs, ys)` is true when both lists have the same cells in order. `List.patch(xs, from, other, replaced)` replaces `replaced` cells from `from` with `other`. Negative `from` / `replaced` is 0. `from` past the end appends `other`. `List.findLast(xs, pred)` is a list of the last match, or empty. `List.prefixLength(xs, pred)` is the length of the leading prefix where `pred` is true. `List.segmentLength(xs, pred, from)` is that length starting at `from`. Negative `from` is 0. `from` past the end is 0. `List.indexOfSlice(xs, slice)` is the first index of `slice`, or `-1`. Empty slice is 0. `List.lastIndexOfSlice(xs, slice)` is the last such index, or `-1`. Empty slice is the length. `List.isDefinedAt(xs, i)` is true when `i` is a valid index. `List.lengthCompare(xs, n)` is negative when the list is shorter than `n`, 0 when equal, and positive when longer. `List.sort(xs)` orders `Int` or `String` cells. Empty stays empty. Equal cells keep their order. `List.sortBy(xs, f)` orders by the `Int` key that `f` returns. `List.max(xs)` / `List.min(xs)` is the greatest / least `Int` or `String` cell. Empty panics. `List.maxBy(xs, f)` / `List.minBy(xs, f)` picks by that `Int` key. A tie keeps the first cell. `List.groupBy(xs, f)` groups cells by the `Int` or `String` key that `f` returns. Map keys sort. Cells in a group keep their order. Empty is empty. `List.sum(xs)` adds `Int` cells. Empty is 0. `List.product(xs)` multiplies `Int` cells. Empty is 1. `IO.foreach(xs, f)` runs `f` on each cell in order and is `IO[List[U]]`. Empty is an empty list. Failure or cancel stops later cells. `IO.foreachDiscard(xs, f)` is `IO[Unit]`. `IO.when(cond, io)` runs `io` when `cond` is true. Else it is `IO.pure(())`. `IO.unless` inverts the cond. `Ref.of(x)` is `IO[Ref[A]]`. `Ref.update(r, f)` / `Ref.updateAndGet(r, f)` apply `f` to the cell. Pin `Queue[Int]` / `Deferred[Int]` with `: IO[Queue[Int]]`. A missing pin is String. `Map.filter(m, pred)` keeps entries whose value matches `pred`. `Map.mapValues(m, f)` maps each value. `Map.exists(m, pred)` is true when any value matches. `Map.forall(m, pred)` is true when every value matches (empty is true). `Set.filter(s, pred)` keeps keys that match `pred`. `Set.map(s, f)` maps each key to an `Int` or `String` key. Duplicate keys collapse. `Set.exists(s, pred)` / `Set.forall(s, pred)` test keys. `Str.startsWith(s, prefix)` is `true` when `s` begins with `prefix`. `Str.contains(s, needle)` is true when `needle` occurs in `s`. `Str.endsWith(s, suffix)` is true when `s` ends with `suffix`. `Str.toInt(s, default)` parses base-10; junk or overflow uses `default`. `Str.replace(s, old, new)` replaces every non-overlapping `old`. Empty `old` leaves `s`. `Str.split(s, sep)` splits on non-overlapping `sep`. Empty `sep` copies `s` as one cell. `Str.isEmpty(s)` is true when `s` is empty. `Str.nonEmpty(s)` is true when `s` is not empty. `Str.toLower(s)` / `Str.toUpper(s)` map ASCII letters. Other bytes stay. `Str.capitalize(s)` maps the first ASCII letter to upper. Other bytes stay. Empty stays empty. `Str.repeat(s, n)` copies `s` `n` times (`n` <= 0 is empty). `Str.byteLen(s)` is the byte count; `Str.byteSlice(s, start, end)` copies that byte range. Use them for protocol framing (LSP `Content-Length` is bytes). All other `Str.*` index by code point. `Str.stripPrefix(s, prefix)` drops `prefix` when `s` starts with it. Else it copies `s`. `Str.stripSuffix(s, suffix)` drops `suffix` when `s` ends with it. Else it copies `s`. `Str.padLeft(s, n, pad)` / `Str.padRight(s, n, pad)` pad to width `n`. `n` <= len, or empty `pad`, copies `s`. `Str.isBlank(s)` is true when `s` has no bytes or only ASCII space, tab, CR, and LF. `Str.lastIndexOf(s, needle)` is the last start code-point index of `needle`, or `-1`. An empty needle is the length of `s`. `Str.take(s, n)` keeps the first `n` code points (`n` <= 0 is empty). `Str.drop(s, n)` skips `n` (`n` <= 0 copies `s`). `Str.takeRight(s, n)` keeps the last `n` code points. `Str.dropRight(s, n)` drops the last `n`. `Str.reverse(s)` reverses the code points. `Str.len(s)` is the code-point count. `Str.charAt(s, i)` is the code point at `i`, or `-1`. `Str.indexOf(s, needle)` is the first start code-point index, or `-1`. `Str.slice(s, start, end)` copies that code-point range. `Str.lines(s)` splits on CR/LF and drops empty lines. `List.reverse(xs)` copies the spine in reverse. `Str.trim(s)` drops leading and trailing ASCII space, tab, CR, and LF. `View.each` lambdas bind the element type. `Net.serve` lambdas bind `(String, String, String)` (path, method, body). `Stream.*` / `Resource.make` / `Resource.use` bind the payload. `Signal.map` / `List.tabulate` bind Int. The lambda body must return the kit result: `View` (`View.each` / `Ui.run`), `Bool` (filter / find / findLast / exists / takeWhile / dropWhile / forall / indexWhere / lastIndexWhere / span / partition / prefixLength / segmentLength / `Map.filter` / `Map.exists` / `Map.forall` / `Set.filter` / `Set.exists` / `Set.forall`), `Int` (`List.sortBy` / `List.maxBy` / `List.minBy`), `Int` or `String` (`List.groupBy` / `List.distinctBy` / `Set.map`), the mapped element (`List.map` / `List.tabulate` / `Map.mapValues` / `Stream.map`), `List` (`List.flatMap`), `String` (`Signal.map`; Int stringifies), or `IO` (`Resource` / `Net` / `Stream.evalMap` / `IO.foreach` / `IO.foreachDiscard`). `View.wrap(…)` lays out children left to right. A child that does not fit the remaining width starts a new run. Wrap sizes to the runs. `View.grid(n, …)` lays out children in `n` columns (`n` < `1` is one column). A new row starts after `n` shown children. Bounded width uses equal column slots. Height sizes to the rows. `View.scroll(child)` pans on y. Content lays out with unbounded height. Wrap a scroll list in `View.expanded(…)` inside a Column so it fills leftover height. `View.scrollH(child)` pans on x. Content lays out with unbounded width. Height sizes to the child. `scroll N dy` pans that scroll on its axis. In a Row, `View.expanded` takes leftover width. Scroll content is unbounded on the pan axis, so a Row inside a List keeps an intrinsic height. Expanded flex slots are tight. `View.stretch(child)` tightens the cross axis in a Column (width) or Row (height); the main axis stays intrinsic. Column and row do not stretch non-flex children unless wrapped in `View.stretch`. `View.center(child)` fills the max slot and centers the child. `View.align(ax, ay, child)` places the child (`0` start / `1` center / `2` end). `View.stack(…)` overlays children. `View.positioned(x, y, child)` offsets a Stack child. `View.padding(n, child)` insets uniformly. `View.sized(w, h, child)` is a tight slot. `View.minSize(w, h, child)` raises min size (`0` = no floor on that axis). `View.maxSize(w, h, child)` lowers max size (`0` = no cap on that axis). Incoming max still wins when tighter. `View.clip(child)` clips paint to the clip frame. Scroll uses the same clip. Do not add constraint-overflow dumps. `View.opacity(pct, child)` scales paint alpha (`0` = transparent, `100` = opaque). Nested opacity multiplies. `View.maxLines(n, child)` keeps at most `n` wrapped text lines (`0` = no cap). Nested caps take the tighter value. A11y still dumps the full string. Buttons and TextField stay one line. `View.ellipsis(child)` keeps extra lines off the paint. Without a positive `maxLines` it keeps one line. With `maxLines` it paints `...` on the last visible line when more text remains. A11y still dumps the full string. `View.textColor(color, child)` paints `View.text` / `View.bindText` with `color`. Nested `textColor` uses the inner color. Buttons and TextField stay on the theme. `View.gap(n, child)` sets Column/Row/Wrap/Grid/List spacing to `n` px (`0` = none). Nested `gap` uses the inner value. Without `View.gap`, Column/Row/Wrap/Grid/List use the theme gap. `View.fontSize(n, child)` sets `View.text` / `View.bindText` measure and paint size (`n` px, min `1`). Nested `fontSize` uses the inner size. Buttons and TextField stay on the theme font. `View.editor(sig)` paints a multiline buffer on `sig`. Insert and delete at the caret include newline and tab (two spaces). A11y dumps one `editor:editor` node. The dump uses `[editor]`, not `[fields]`. `View.split(frac, start, end)` is a row with a drag handle. `frac` is 0–100. `[splits]` dumps `N frac=F`. `View.overlay(open, child)` fills the parent when `open` is not 0. Compose it on `View.stack`. Escape and a backdrop tap write 0. Keys go to the overlay subtree while it is open. `[overlays]` dumps `N* open=0|1`. `View.focusGroup(child)` sizes to `child`. A tap on a descendant tap target focuses that list. ArrowUp / ArrowDown move among sibling taps when no overlay is open. Enter / Space activate the focused row. An open overlay still takes keys. `[session]` dumps `focus=button: