diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index 265e617b..257a104d 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -862,7 +862,10 @@ SzString *sz_fs_basename(SzString *path); /* Pure Json. Enum Null|Bool|Int|Float|Str|Arr|Obj. * parse / stringify return Result (Err=0 / Ok=1). Query kits return empty - * lists on a miss or a wrong tag. `*_or` / `get_*` use the default. + * lists on a miss or a wrong tag. is* and as* stay tag-strict. + * intOr / floatOr / getInt / getFloat coerce JSON numbers. Int to Float + * is a C double cast. Float to Int is an exact i64, else the default. + * Other *_or / get_* use the default. * Write kits copy Obj / Arr cells. A miss keeps the default or retains `j`. */ SzAdt *sz_json_parse(SzString *s); SzAdt *sz_json_stringify(SzAdt *j); @@ -875,6 +878,7 @@ SzList *sz_json_pairs(SzAdt *j); /* List[(String, Json)]; empty if int64_t sz_json_is_null(SzAdt *j); int64_t sz_json_is_bool(SzAdt *j); int64_t sz_json_is_int(SzAdt *j); +int64_t sz_json_is_float(SzAdt *j); int64_t sz_json_is_str(SzAdt *j); int64_t sz_json_is_arr(SzAdt *j); int64_t sz_json_is_obj(SzAdt *j); diff --git a/crates/runtime/include/scuzz_ui.h b/crates/runtime/include/scuzz_ui.h index 675b70b9..3640e156 100644 --- a/crates/runtime/include/scuzz_ui.h +++ b/crates/runtime/include/scuzz_ui.h @@ -132,6 +132,7 @@ SzString *sz_signal_dump(void); /* Publish the for-binder name. Property and Timeline kits read that name. */ void sz_signal_name(const void *sig, const char *name); +/* Named observation. A missing name panics. */ int64_t sz_property_signal_int(SzString *name); SzString *sz_property_signal_str(SzString *name); int64_t sz_property_signal_list_len(SzString *name); @@ -729,6 +730,7 @@ SzString *sz_lang_signal_str_get(SzSignalStr *s); void *sz_lang_signal_str_set(SzSignalStr *s, SzString *v); SzSignalList *sz_lang_signal_list(SzList *initial, SzString *name, int64_t elem_str); +/* Retain the current list. Last-use drops it. */ SzList *sz_lang_signal_list_get(SzSignalList *s); void *sz_lang_signal_list_set(SzSignalList *s, SzList *v); diff --git a/crates/runtime/src/json.c b/crates/runtime/src/json.c index d2d689bb..8898cf46 100644 --- a/crates/runtime/src/json.c +++ b/crates/runtime/src/json.c @@ -804,6 +804,7 @@ SzList *sz_json_pairs(SzAdt *j) { int64_t sz_json_is_null(SzAdt *j) { return json_tag(j) == JSON_NULL ? 1 : 0; } int64_t sz_json_is_bool(SzAdt *j) { return json_tag(j) == JSON_BOOL ? 1 : 0; } int64_t sz_json_is_int(SzAdt *j) { return json_tag(j) == JSON_INT ? 1 : 0; } +int64_t sz_json_is_float(SzAdt *j) { return json_tag(j) == JSON_FLOAT ? 1 : 0; } int64_t sz_json_is_str(SzAdt *j) { return json_tag(j) == JSON_STR ? 1 : 0; } int64_t sz_json_is_arr(SzAdt *j) { return json_tag(j) == JSON_ARR ? 1 : 0; } int64_t sz_json_is_obj(SzAdt *j) { return json_tag(j) == JSON_OBJ ? 1 : 0; } @@ -831,16 +832,27 @@ int64_t sz_json_bool_or(SzAdt *j, int64_t d) { return n; } +static int json_float_exact_i64(double x, int64_t *out) { + int64_t n; + if (!isfinite(x)) + return 0; + if (x < (double)INT64_MIN || x >= 0x1p63) + return 0; + n = (int64_t)x; + if ((double)n != x) + return 0; + *out = n; + return 1; +} + int64_t sz_json_int_or(SzAdt *j, int64_t d) { - SzList *xs = sz_json_as_int(j); + int tag = json_tag(j); int64_t n; - if (!xs) { - sz_release(xs); - return d; - } - n = sz_unbox_i64(sz_list_head(xs)); - sz_release(xs); - return n; + if (tag == JSON_INT) + return sz_unbox_i64(sz_adt_payload(j)); + if (tag == JSON_FLOAT && json_float_exact_i64(unbox_f64(sz_adt_payload(j)), &n)) + return n; + return d; } SzString *sz_json_str_or(SzAdt *j, SzString *d) { @@ -938,15 +950,12 @@ SzAdt *sz_json_merge(SzAdt *a, SzAdt *b) { } double sz_json_float_or(SzAdt *j, double d) { - SzList *xs = sz_json_as_float(j); - double x; - if (!xs) { - sz_release(xs); - return d; - } - x = unbox_f64(sz_list_head(xs)); - sz_release(xs); - return x; + int tag = json_tag(j); + if (tag == JSON_FLOAT) + return unbox_f64(sz_adt_payload(j)); + if (tag == JSON_INT) + return (double)sz_unbox_i64(sz_adt_payload(j)); + return d; } double sz_json_get_float(SzAdt *j, SzString *key, double d) { diff --git a/crates/runtime/src/runtime.c b/crates/runtime/src/runtime.c index 7744c8d8..b8c23b50 100644 --- a/crates/runtime/src/runtime.c +++ b/crates/runtime/src/runtime.c @@ -2528,7 +2528,7 @@ static SzIo *ensure_run_fin_err(SzIo *fin, SzError *err) { } static SzIo *ignore_then_io(void *ignored, void *env) { - (void)ignored; + sz_release(ignored); return (SzIo *)env; } diff --git a/crates/runtime/src/signal.c b/crates/runtime/src/signal.c index a6a70006..7329edcc 100644 --- a/crates/runtime/src/signal.c +++ b/crates/runtime/src/signal.c @@ -104,6 +104,23 @@ static SigReg *sig_find(SigKind kind, const char *name) { return NULL; } +static void sig_missing(const char *name) { + char buf[192]; + snprintf(buf, sizeof buf, "missing signal %s", + name && name[0] ? name : "(empty)"); + sz_panic(buf); +} + +/* First non-null head decides String vs count-only. Empty keeps `unknown`. */ +static int sig_list_heads_str(const SzList *p, int unknown) { + for (; p; p = p->tail) { + if (!p->head) + continue; + return sz_rc_kind(p->head) == SZ_RC_STRING; + } + return unknown ? 1 : 0; +} + /* Mark a list signal's element kind: 1 = String (dump prints elements), * 0 = other (dump prints the count only). */ static void sig_set_elem_str(const void *sig, int64_t elem_str) { @@ -144,7 +161,7 @@ SzString *sz_signal_dump(void) { break; case SIG_LIST: { SzList *p = sz_signal_list_get((const SzSignalList *)r->sig); - if (!r->elem_str) { + if (!sig_list_heads_str(p, r->elem_str)) { snprintf(line, sizeof line, "list[%d] %s= <%lld>\n", r->id, tag, (long long)sz_list_len(p)); sz_dump_append(&buf, &len, &cap, line); @@ -175,7 +192,9 @@ int64_t sz_property_signal_int(SzString *name) { if (sz_timeline_replaying()) return sz_timeline_replay_signal_int(n); r = sig_find(SIG_INT, n); - return r ? sz_signal_int_get((const SzSignalInt *)r->sig) : 0; + if (!r) + sig_missing(n); + return sz_signal_int_get((const SzSignalInt *)r->sig); } SzString *sz_property_signal_str(SzString *name) { @@ -184,10 +203,9 @@ SzString *sz_property_signal_str(SzString *name) { 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)); - return sz_string_from_cstr(""); + if (!r) + sig_missing(n); + return sz_string_from_cstr(sz_signal_str_get((const SzSignalStr *)r->sig)); } int64_t sz_property_signal_list_len(SzString *name) { @@ -196,10 +214,9 @@ int64_t sz_property_signal_list_len(SzString *name) { 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)); - return 0; + if (!r) + sig_missing(n); + return (int64_t)sz_list_len(sz_signal_list_get((const SzSignalList *)r->sig)); } SzString *sz_property_signal_list_at(SzString *name, int64_t index) { @@ -212,17 +229,19 @@ SzString *sz_property_signal_list_at(SzString *name, int64_t index) { 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; - while (p) { - if (i == index) { - SzString *h = (SzString *)p->head; - return sz_string_from_cstr(h ? sz_string_cstr(h) : ""); - } - p = p->tail; - i++; + if (!r) + sig_missing(n); + p = sz_signal_list_get((const SzSignalList *)r->sig); + if (!sig_list_heads_str(p, r->elem_str)) + return sz_string_from_cstr(""); + i = 0; + while (p) { + if (i == index) { + SzString *h = (SzString *)p->head; + return sz_string_from_cstr(h ? sz_string_cstr(h) : ""); } + p = p->tail; + i++; } return sz_string_from_cstr(""); } @@ -323,9 +342,9 @@ int sz_signal_list_elem_str(const SzSignalList *s) { SigReg *r; for (r = g_sig_head; r; r = r->next) { if (r->sig == (const void *)s) - return r->elem_str; + return sig_list_heads_str(s ? s->value : NULL, r->elem_str); } - return 1; + return 0; } SzSignalList *sz_lang_signal_list(SzList *initial, SzString *name, @@ -337,7 +356,12 @@ SzSignalList *sz_lang_signal_list(SzList *initial, SzString *name, return s; } -SzList *sz_lang_signal_list_get(SzSignalList *s) { return sz_signal_list_get(s); } +/* Retain so last-use can drop. The C getter stays a borrow. */ +SzList *sz_lang_signal_list_get(SzSignalList *s) { + SzList *xs = sz_signal_list_get(s); + sz_retain(xs); + return xs; +} void *sz_lang_signal_list_set(SzSignalList *s, SzList *v) { sz_signal_list_set(s, v); diff --git a/crates/runtime/src/stream.c b/crates/runtime/src/stream.c index 0cd37392..600c0c57 100644 --- a/crates/runtime/src/stream.c +++ b/crates/runtime/src/stream.c @@ -282,6 +282,15 @@ typedef struct StFilter { int64_t acc_len; } StFilter; +typedef struct StPull { + SzStream *s; + SzStreamPred pred; + void *penv; + int64_t remain; + int64_t acc_len; + int *found; +} StPull; + typedef struct StMap { SzCont f; void *fenv; @@ -346,6 +355,8 @@ static SzIo *find_into(SzStream *s, SzList *acc, int64_t remain, SzStreamPred pred, void *penv, int *found); static SzIo *filter_into(SzStream *s, SzList *acc, int64_t remain, SzStreamPred pred, void *penv); +static SzList *find_added(SzList *acc, int64_t acc_len, SzStreamPred pred, + void *penv, int64_t remain, int *found); static SzIo *dropwhile_into(SzStream *s, SzList *acc, int64_t remain, SzStreamPred pred, void *penv); static SzIo *after_or_else(void *acc, void *env); @@ -513,6 +524,88 @@ static SzIo *after_filter(void *acc, void *env) { return pure_drop(out); } +static SzIo *after_stream_pin(void *acc, void *env) { + sz_release(env); + return pure_drop(acc); +} + +static SzIo *after_filter_one(void *acc, void *env) { + StPull *st = (StPull *)env; + SzStream *s = st->s; + SzStreamPred pred = st->pred; + void *penv = st->penv; + int64_t remain = st->remain; + int64_t acc_len = st->acc_len; + int64_t added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + SzList *out; + sz_free(st); + out = filter_added((SzList *)acc, acc_len, pred, penv, remain); + if (added <= 0) + return pure_drop(out); + if (remain >= 0) { + int64_t kept = (int64_t)sz_list_len(out) - acc_len; + remain = remain - kept; + if (remain < 0) + remain = 0; + } + if (remain == 0) + return pure_drop(out); + { + SzStream *rest = sz_stream_drop(s, 1); + SzIo *io = fm_drop(filter_into(rest, out, remain, pred, penv), + after_stream_pin, rest); + sz_release(rest); + return io; + } +} + +static SzIo *filter_pull_one(SzStream *s, SzList *acc, int64_t remain, + SzStreamPred pred, void *penv) { + StPull *st = (StPull *)sz_alloc(sizeof(StPull)); + st->s = s; + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->found = NULL; + return fm_drop(compile_into(s, acc, 1), after_filter_one, st); +} + +static SzIo *after_find_one(void *acc, void *env) { + StPull *st = (StPull *)env; + SzStream *s = st->s; + SzStreamPred pred = st->pred; + void *penv = st->penv; + int64_t remain = st->remain; + int64_t acc_len = st->acc_len; + int *found = st->found; + int64_t added = (int64_t)sz_list_len((SzList *)acc) - acc_len; + SzList *out; + sz_free(st); + out = find_added((SzList *)acc, acc_len, pred, penv, remain, found); + if ((found && *found) || added <= 0) + return pure_drop(out); + { + SzStream *rest = sz_stream_drop(s, 1); + SzIo *io = fm_drop(find_into(rest, out, remain, pred, penv, found), + after_stream_pin, rest); + sz_release(rest); + return io; + } +} + +static SzIo *find_pull_one(SzStream *s, SzList *acc, int64_t remain, + SzStreamPred pred, void *penv, int *found) { + StPull *st = (StPull *)sz_alloc(sizeof(StPull)); + st->s = s; + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->found = found; + return fm_drop(compile_into(s, acc, 1), after_find_one, st); +} + static SzIo *fold_evalmap(SzList *xs, StMap *st); static SzIo *after_map_one(void *value, void *env) { @@ -825,12 +918,16 @@ static SzIo *filter_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StFilter *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + /* Keep take(n) as an inner item budget. Do not fold n into remain. */ + st = (StFilter *)sz_alloc(sizeof(StFilter)); + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + return fm_drop(compile_into(s, acc, n), after_filter, st); } case SZ_ST_CONS: if (pred(s->left, penv) != 0) { @@ -839,6 +936,65 @@ static SzIo *filter_into(SzStream *s, SzList *acc, int64_t remain, } s = (SzStream *)s->right; 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); + if (pred(box, penv) != 0) { + acc = cons_take(box, acc); + remain = remain_dec(remain); + } + sz_release(box); + from++; + } + return pure_drop(acc); + } + case SZ_ST_MAP: { + SzStreamMapFn mf = (SzStreamMapFn)s->right; + void *menv = s->env; + SzStream *in = (SzStream *)s->left; + if (in && in->tag == SZ_ST_RANGE) { + int64_t from = (int64_t)(intptr_t)in->env; + int64_t until = (int64_t)(intptr_t)in->right; + while (from < until && remain != 0) { + void *box = sz_box_i64(from); + void *mapped = mf(box, menv); + sz_release(box); + if (pred(mapped, penv) != 0) { + acc = cons_take(mapped, acc); + remain = remain_dec(remain); + } + sz_release(mapped); + from++; + } + return pure_drop(acc); + } + return filter_pull_one(s, acc, remain, pred, penv); + } + case SZ_ST_DROP: { + int64_t n = (int64_t)(intptr_t)s->env; + SzStream *inner = (SzStream *)s->left; + if (n <= 0) { + s = inner; + break; + } + if (inner && inner->tag == SZ_ST_RANGE) { + int64_t from = (int64_t)(intptr_t)inner->env + n; + int64_t until = (int64_t)(intptr_t)inner->right; + while (from < until && remain != 0) { + void *box = sz_box_i64(from); + if (pred(box, penv) != 0) { + acc = cons_take(box, acc); + remain = remain_dec(remain); + } + sz_release(box); + from++; + } + return pure_drop(acc); + } + return filter_pull_one(s, acc, remain, pred, penv); + } case SZ_ST_EVAL: { StTWEval *st = (StTWEval *)sz_alloc(sizeof(StTWEval)); st->tail = (SzStream *)s->right; @@ -860,15 +1016,8 @@ static SzIo *filter_into(SzStream *s, SzList *acc, int64_t remain, return fm_drop(filter_into((SzStream *)s->left, acc, remain, pred, penv), after_filter_concat, st); } - default: { - /* Nested map and other tags still compile then cut. */ - StFilter *st = (StFilter *)sz_alloc(sizeof(StFilter)); - st->pred = pred; - st->penv = penv; - st->remain = remain; - st->acc_len = (int64_t)sz_list_len(acc); - return fm_drop(compile_into(s, acc, remain), after_filter, st); - } + default: + return filter_pull_one(s, acc, remain, pred, penv); } } return pure_drop(acc); @@ -921,12 +1070,15 @@ static SzIo *dropwhile_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StDropWhile *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + st = (StDropWhile *)sz_alloc(sizeof(StDropWhile)); + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + return fm_drop(compile_into(s, acc, n), after_dropwhile, st); } case SZ_ST_CONS: if (pred(s->left, penv) != 0) { @@ -935,6 +1087,30 @@ static SzIo *dropwhile_into(SzStream *s, SzList *acc, int64_t remain, } acc = cons_take(s->left, acc); return compile_into((SzStream *)s->right, acc, remain_dec(remain)); + 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) { + void *box = sz_box_i64(from); + if (pred(box, penv) == 0) { + acc = cons_take(box, acc); + sz_release(box); + from++; + remain = remain_dec(remain); + while (from < until && remain != 0) { + box = sz_box_i64(from); + acc = cons_take(box, acc); + sz_release(box); + from++; + remain = remain_dec(remain); + } + return pure_drop(acc); + } + sz_release(box); + from++; + } + return pure_drop(acc); + } case SZ_ST_EVAL: { StTWEval *st = (StTWEval *)sz_alloc(sizeof(StTWEval)); st->tail = (SzStream *)s->right; @@ -963,7 +1139,7 @@ static SzIo *dropwhile_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_dropwhile, st); + return fm_drop(compile_into(s, acc, remain), after_dropwhile, st); } } } @@ -1454,12 +1630,16 @@ static SzIo *mapconcat_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StLift *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + 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(), n), after_lift, st); } case SZ_ST_CONS: acc = mc_apply(acc, s->left, f, fenv, &remain); @@ -1561,12 +1741,16 @@ static SzIo *changes_into(SzStream *s, SzList *acc, int64_t remain, void *prev, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StLift *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + 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(), n), after_lift, st); } case SZ_ST_CONS: if (!have || !sz_ptr_eq(prev, s->left)) { @@ -1699,12 +1883,16 @@ static SzIo *flatmap_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StLift *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + 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(), n), after_lift, st); } case SZ_ST_CONS: return flatmap_one(s->left, acc, remain, (SzStream *)s->right, f, fenv, 0); @@ -1823,12 +2011,16 @@ static SzIo *takewhile_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StTWCut *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + st = (StTWCut *)sz_alloc(sizeof(StTWCut)); + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->stopped = stopped; + return fm_drop(compile_into(s, acc, n), after_tw_cut, st); } case SZ_ST_CONS: if (pred(s->left, penv) == 0) { @@ -1941,12 +2133,16 @@ static SzIo *find_into(SzStream *s, SzList *acc, int64_t remain, switch (s->tag) { case SZ_ST_TAKE: { int64_t n = (int64_t)(intptr_t)s->env; + StTWCut *st; if (n <= 0) return pure_drop(acc); - if (remain < 0 || n < remain) - remain = n; - s = (SzStream *)s->left; - break; + st = (StTWCut *)sz_alloc(sizeof(StTWCut)); + st->pred = pred; + st->penv = penv; + st->remain = remain; + st->acc_len = (int64_t)sz_list_len(acc); + st->stopped = found; + return fm_drop(compile_into(s, acc, n), after_find_cut, st); } case SZ_ST_CONS: if (pred(s->left, penv) != 0) { @@ -1958,6 +2154,92 @@ static SzIo *find_into(SzStream *s, SzList *acc, int64_t remain, } s = (SzStream *)s->right; 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) { + void *box = sz_box_i64(from); + if (pred(box, penv) != 0) { + if (found) + *found = 1; + if (remain == 0) { + sz_release(box); + return pure_drop(acc); + } + { + SzList *out = cons_take(box, acc); + sz_release(box); + return pure_drop(out); + } + } + sz_release(box); + from++; + } + return pure_drop(acc); + } + case SZ_ST_MAP: { + SzStreamMapFn mf = (SzStreamMapFn)s->right; + void *menv = s->env; + SzStream *in = (SzStream *)s->left; + if (in && in->tag == SZ_ST_RANGE) { + int64_t from = (int64_t)(intptr_t)in->env; + int64_t until = (int64_t)(intptr_t)in->right; + while (from < until) { + void *box = sz_box_i64(from); + void *mapped = mf(box, menv); + sz_release(box); + if (pred(mapped, penv) != 0) { + if (found) + *found = 1; + if (remain == 0) { + sz_release(mapped); + return pure_drop(acc); + } + { + SzList *out = cons_take(mapped, acc); + sz_release(mapped); + return pure_drop(out); + } + } + sz_release(mapped); + from++; + } + return pure_drop(acc); + } + return find_pull_one(s, acc, remain, pred, penv, found); + } + case SZ_ST_DROP: { + int64_t n = (int64_t)(intptr_t)s->env; + SzStream *inner = (SzStream *)s->left; + if (n <= 0) { + s = inner; + break; + } + if (inner && inner->tag == SZ_ST_RANGE) { + int64_t from = (int64_t)(intptr_t)inner->env + n; + int64_t until = (int64_t)(intptr_t)inner->right; + while (from < until) { + void *box = sz_box_i64(from); + if (pred(box, penv) != 0) { + if (found) + *found = 1; + if (remain == 0) { + sz_release(box); + return pure_drop(acc); + } + { + SzList *out = cons_take(box, acc); + sz_release(box); + return pure_drop(out); + } + } + sz_release(box); + from++; + } + return pure_drop(acc); + } + return find_pull_one(s, acc, remain, pred, penv, found); + } case SZ_ST_EVAL: { StTWEval *st = (StTWEval *)sz_alloc(sizeof(StTWEval)); st->tail = (SzStream *)s->right; @@ -1980,15 +2262,8 @@ static SzIo *find_into(SzStream *s, SzList *acc, int64_t remain, find_into((SzStream *)s->left, acc, remain, pred, penv, found), after_find_concat, st); } - default: { - StTWCut *st = (StTWCut *)sz_alloc(sizeof(StTWCut)); - st->pred = pred; - st->penv = penv; - st->remain = remain; - st->acc_len = (int64_t)sz_list_len(acc); - st->stopped = found; - return fm_drop(compile_into(s, acc, remain), after_find_cut, st); - } + default: + return find_pull_one(s, acc, remain, pred, penv, found); } } return pure_drop(acc); diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index 7d8eb308..6e5859e3 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -3220,9 +3220,18 @@ static const char *tl_sig_line(const char *dump, const char *kind, return NULL; } +static void tl_missing(const char *name) { + char buf[192]; + snprintf(buf, sizeof buf, "missing signal %s", + name && name[0] ? name : "(empty)"); + sz_panic(buf); +} + int64_t sz_timeline_replay_signal_int(const char *name) { const char *sep = tl_sig_line(g_replay_signals, "int", name); - return sep ? (int64_t)atoll(sep + 3) : 0; + if (!sep) + tl_missing(name); + return (int64_t)atoll(sep + 3); } static const char *tl_sig_payload(const char *sep) { @@ -3263,18 +3272,23 @@ static int64_t tl_count_quoted_list(const char *p) { 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; + if (!sep) + tl_missing(name); + return (int64_t)atoll(sep + 3); } SzString *sz_timeline_replay_signal_str(const char *name) { - return tl_parse_quoted_str(tl_sig_line(g_replay_signals, "str", name)); + const char *sep = tl_sig_line(g_replay_signals, "str", name); + if (!sep) + tl_missing(name); + return tl_parse_quoted_str(sep); } 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; + tl_missing(name); if (memcmp(sep, " = <", 4) == 0) return (int64_t)atoll(sep + 4); if (memcmp(sep, " = [", 4) != 0) @@ -3287,7 +3301,9 @@ 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) + if (!sep) + tl_missing(name); + if (index < 0 || memcmp(sep, " = [", 4) != 0) return sz_string_from_cstr(""); p = sep + 4; while (*p && *p != ']' && *p != '\n') { @@ -3336,7 +3352,7 @@ 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; if (!sep) - return 0; + tl_missing(name); /* Record lists dump the count only: `list[] = `. */ if (memcmp(sep, " = <", 4) == 0) return (int64_t)atoll(sep + 4); @@ -3366,6 +3382,8 @@ int64_t sz_timeline_signal_str_has(void *tl, int64_t i, SzString *name, if (!s || !s->signals) return 0; sep = tl_sig_line(s->signals, "str", name ? sz_string_cstr(name) : ""); + if (!sep) + tl_missing(name ? sz_string_cstr(name) : ""); p = tl_sig_payload(sep); if (!p || *p != '"') return 0; diff --git a/crates/runtime/src/ui.c b/crates/runtime/src/ui.c index 4392c4e1..f066d0d1 100644 --- a/crates/runtime/src/ui.c +++ b/crates/runtime/src/ui.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include "scuzz_ui.h" #include "scuzz_embedder.h" @@ -15,6 +16,7 @@ #include #include #include +#include static int want_gpu_presenter(void) { const char *e = getenv("SCUZZ_SKIA"); @@ -1455,7 +1457,9 @@ int sz_ui_pump_sync(SzUiSession *session) { sz_view_layout(session->root, (float)session->cfg.width, (float)session->cfg.height, session->theme); pthread_mutex_lock(&session->bridge_lock); - session->dirty = 0; + /* A post during paint leaves bridge work. Keep dirty so the next pump + * and quiesce sample that work. */ + session->dirty = session->bridge_head != NULL; pthread_mutex_unlock(&session->bridge_lock); /* Desktop peer: present to OS surface when embedder is available. */ if (session->cfg.kind == SZ_UI_RUNTIME_DESKTOP && sz_embedder_available()) { @@ -2245,11 +2249,22 @@ SzQuiesce sz_ui_quiesce(SzUiSession *session) { pthread_mutex_unlock(&session->bridge_lock); if (!sz_ui_pump_sync(session)) sz_panic("Ui.run quiesce pump failed"); - /* Let IO bridge posters run. Count work that arrived during the pump. */ + /* Let IO bridge posters run. Count work that arrived during the pump. + * sched_yield can skip a runnable poster; wait 1 ms before an idle + * sample so a live poster is visible. */ sched_yield(); pthread_mutex_lock(&session->bridge_lock); busy = busy || session->dirty || session->bridge_head != NULL; pthread_mutex_unlock(&session->bridge_lock); + if (!busy) { + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 1000 * 1000; + nanosleep(&ts, NULL); + pthread_mutex_lock(&session->bridge_lock); + busy = session->dirty || session->bridge_head != NULL; + pthread_mutex_unlock(&session->bridge_lock); + } if (busy) idle = 0; else { diff --git a/crates/runtime/tests/test_io.c b/crates/runtime/tests/test_io.c index ee755729..3932b4b9 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -366,6 +366,27 @@ static int64_t stream_empty(void *v, void *env) { return sz_string_len((SzString *)v) == 0; } +static int64_t stream_even_i64(void *v, void *env) { + (void)env; + return (sz_unbox_i64(v) & 1) == 0; +} + +static int64_t stream_gt5_i64(void *v, void *env) { + (void)env; + return sz_unbox_i64(v) > 5; +} + +static int64_t stream_eq1_i64(void *v, void *env) { + (void)env; + return sz_unbox_i64(v) == 1; +} + +static int64_t stream_pred_false(void *v, void *env) { + (void)v; + (void)env; + return 0; +} + static void *stream_dup(void *v, void *env) { (void)env; return sz_stream_concat(sz_stream_emit(v), sz_stream_emit(v)); @@ -2826,6 +2847,35 @@ int main(void) { assert(live_bytes == base_bytes); } + /* Nested ensure: cancel drops the inner finalizer success value. */ + { + SzIo *park; + SzIo *inner_fin; + SzIo *inner; + SzIo *outer_fin; + SzIo *nested; + size_t base_bytes = 0, base_count = 0; + size_t live_bytes = 0, live_count = 0; + + sz_testrt_install(); + sz_alloc_stats(&base_bytes, &base_count); + park = sz_io_sleep_ms(100); + inner_fin = pure_drop(sz_string_from_cstr("inner-fin")); + inner = sz_io_ensure(park, inner_fin); + sz_release(park); + sz_release(inner_fin); + outer_fin = pure_drop(NULL); + nested = sz_io_ensure(inner, outer_fin); + sz_release(inner); + sz_release(outer_fin); + r = sz_io_unsafe_run(fm_drop(fork_drop(nested), fiber_interrupt_direct, NULL)); + assert(r.ok); + sz_alloc_stats(&live_bytes, &live_count); + assert(live_count == base_count); + assert(live_bytes == base_bytes); + sz_testrt_reset(); + } + /* Resource releases when cancelled as race loser (TestRuntime: both park, then short wins). */ { sz_testrt_install(); @@ -4032,6 +4082,42 @@ int main(void) { sz_release(ar); } + /* isFloat is tag-strict. getInt / getFloat coerce JSON numbers. */ + { + SzAdt *fr = json_expect_ok("1.5"); + SzAdt *ir = json_expect_ok("1"); + SzAdt *f = (SzAdt *)sz_adt_payload(fr); + SzAdt *n = (SzAdt *)sz_adt_payload(ir); + SzAdt *o10 = json_expect_ok("{\"n\":1.0}"); + SzAdt *oe2 = json_expect_ok("{\"n\":1e2}"); + SzAdt *oi = json_expect_ok("{\"n\":1}"); + SzAdt *o15 = json_expect_ok("{\"n\":1.5}"); + SzString *kn = sz_string_from_cstr("n"); + SzList *as_i = sz_json_as_int(f); + SzList *as_f = sz_json_as_float(n); + double gf; + assert(sz_json_is_float(f) == 1); + assert(sz_json_is_float(n) == 0); + assert(sz_json_is_int(n) == 1); + assert(sz_json_is_int(f) == 0); + assert(sz_list_is_empty(as_i)); + assert(sz_list_is_empty(as_f)); + assert(sz_json_get_int((SzAdt *)sz_adt_payload(o10), kn, -1) == 1); + assert(sz_json_get_int((SzAdt *)sz_adt_payload(oe2), kn, -1) == 100); + gf = sz_json_get_float((SzAdt *)sz_adt_payload(oi), kn, -1.0); + assert(gf > 0.9 && gf < 1.1); + assert(sz_json_get_int((SzAdt *)sz_adt_payload(o15), kn, -1) == -1); + sz_release(as_i); + sz_release(as_f); + sz_release(kn); + sz_release(fr); + sz_release(ir); + sz_release(o10); + sz_release(oe2); + sz_release(oi); + sz_release(o15); + } + { size_t base_bytes = 0, base_count = 0; size_t live_bytes = 0, live_count = 0; @@ -4213,6 +4299,74 @@ int main(void) { assert(strcmp(sz_string_cstr(joined), "a") == 0); assert(delay_calls == 1); + { + SzList *nums = sz_list_cons( + sz_box_i64(1), + sz_list_cons(sz_box_i64(2), + sz_list_cons(sz_box_i64(3), + sz_list_cons(sz_box_i64(4), sz_list_nil())))); + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_filter( + sz_stream_take(sz_stream_emits(nums), 2), stream_even_i64, NULL))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 2); + } + + { + SzList *nums = sz_list_cons( + sz_box_i64(0), + sz_list_cons(sz_box_i64(0), + sz_list_cons(sz_box_i64(1), sz_list_nil()))); + r = sz_io_unsafe_run(sz_stream_exists( + sz_stream_take(sz_stream_emits(nums), 2), stream_eq1_i64, NULL)); + assert(r.ok); + assert(sz_unbox_i64(r.value) == 0); + } + + { + SzList *cs = sz_list_cons( + sz_string_from_cstr("a"), + sz_list_cons(sz_string_from_cstr("b"), + sz_list_cons(sz_string_from_cstr("c"), sz_list_nil()))); + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_flatmap( + sz_stream_take(sz_stream_emits(cs), 2), stream_dup, NULL))); + assert(r.ok); + joined = test_list_join((SzList *)r.value, ","); + assert(strcmp(sz_string_cstr(joined), "a,a,b,b") == 0); + } + + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_take( + sz_stream_filter(sz_stream_range(0, 100), stream_gt5_i64, NULL), 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 6); + + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_take( + sz_stream_filter(sz_stream_map(sz_stream_range(0, 100), stream_inc, NULL), + stream_gt5_i64, NULL), + 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(sz_unbox_i64(sz_list_head((SzList *)r.value)) == 6); + + r = sz_io_unsafe_run(sz_stream_compile_to_list(sz_stream_take( + sz_stream_dropwhile(sz_stream_range(0, 1000000), stream_pred_false, + NULL), + 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + 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_dropwhile( + sz_stream_map(sz_stream_range(0, 1000000), stream_inc_count, NULL), + stream_pred_false, NULL), + 1))); + assert(r.ok); + assert(sz_list_len((SzList *)r.value) == 1); + assert(iterate_calls == 1); + xs = sz_list_cons( sz_string_from_cstr("a"), sz_list_cons(sz_string_from_cstr("b"), sz_list_nil())); @@ -11561,10 +11715,7 @@ int main(void) { needle = sz_string_from_cstr("tasks"); assert(sz_timeline_signal_list_len(tl, 0, needle) == 3); sz_release(needle); - /* An unnamed line or an unknown name reads as 0. */ - needle = sz_string_from_cstr("missing"); - assert(sz_timeline_signal_int(tl, 0, needle) == 0); - sz_release(needle); + /* A missing name panics (see test_property_missing_signal_panics). */ needle = sz_string_from_cstr("button:+1"); assert(sz_timeline_a11y_has(tl, 0, needle) == 1); sz_release(needle); diff --git a/crates/runtime/tests/test_ui.c b/crates/runtime/tests/test_ui.c index 80ea22b3..d2d60ca6 100644 --- a/crates/runtime/tests/test_ui.c +++ b/crates/runtime/tests/test_ui.c @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -515,8 +517,8 @@ static void test_quiesce(void) { sz_signal_int_free(sig); /* Pending bridge work every pump trips the 64-pump budget. The poster - * thread runs at the same time as pump flushes. Wait for the first post - * so two idle samples cannot settle before the poster runs. */ + * thread runs at the same time as pump flushes. Wait until several posts + * land so two idle samples cannot settle before the poster runs. */ sig = sz_signal_int(0); root = sz_view_column(); sz_view_add_child(root, sz_view_text_signal_int(sig, "n=")); @@ -528,7 +530,7 @@ static void test_quiesce(void) { atomic_init(&env.posted, 0); sz_ui_bridge_post_int(session, sig, 0); assert(pthread_create(&th, NULL, quiesce_poster, &env) == 0); - while (atomic_load_explicit(&env.posted, memory_order_relaxed) == 0) + while (atomic_load_explicit(&env.posted, memory_order_relaxed) < 16) sched_yield(); q = SZ_QUIESCE_SETTLED; for (tries = 0; tries < 8 && q != SZ_QUIESCE_BUDGET_TRIPPED; tries++) @@ -13217,9 +13219,6 @@ static void test_property_signal_list_len(void) { sz_signal_list_set(items, sz_list_nil()); assert(sz_property_signal_list_len(name) == 0); sz_release(name); - name = sz_string_from_cstr("missing"); - assert(sz_property_signal_list_len(name) == 0); - sz_release(name); sz_string_free(dump); sz_signal_list_free(items); } @@ -13253,11 +13252,6 @@ static void test_property_signal_list_at(void) { assert(strcmp(sz_string_cstr(got), "") == 0); sz_string_free(got); sz_release(name); - name = sz_string_from_cstr("missing"); - got = sz_property_signal_list_at(name, 0); - assert(strcmp(sz_string_cstr(got), "") == 0); - sz_string_free(got); - sz_release(name); sz_string_free(dump); sz_signal_list_free(items); } @@ -13272,10 +13266,9 @@ static void test_property_signal_int(void) { dump = sz_signal_dump(); s = strstr(sz_string_cstr(dump), "int["); assert(s); - /* Unnamed signals dump with no name and read as 0 by name. */ + /* Unnamed signals dump with no name. A missing name panics. */ assert(strstr(s, " = 7")); name = sz_string_from_cstr("count"); - assert(sz_property_signal_int(name) == 0); sz_signal_name(count, "count"); assert(sz_property_signal_int(name) == 7); sz_signal_int_set(count, 9); @@ -13308,6 +13301,112 @@ static void test_signal_list_record_dump(void) { sz_release(name); sz_string_free(dump); sz_signal_list_free(items); + + xs = sz_list_cons(sz_box_i64(1), sz_list_nil()); + items = sz_lang_signal_list(xs, sz_string_from_cstr("rows"), 1); + dump = sz_signal_dump(); + s = strstr(sz_string_cstr(dump), "list["); + assert(s); + /* A String flag on non-String heads still dumps the count. */ + assert(strstr(s, "rows = <1>")); + sz_string_free(dump); + sz_signal_list_free(items); +} + +static int wait_aborted(pid_t pid) { + int st = 0; + if (waitpid(pid, &st, 0) != pid) + return 0; + return WIFSIGNALED(st) && WTERMSIG(st) == SIGABRT; +} + +static void assert_missing_aborts(void (*fn)(void)) { + pid_t pid; + int fds[2]; + char err[4096]; + ssize_t n; + + assert(pipe(fds) == 0); + fflush(NULL); + pid = fork(); + assert(pid >= 0); + if (pid == 0) { + dup2(fds[1], STDERR_FILENO); + close(fds[0]); + close(fds[1]); + fn(); + _exit(1); + } + close(fds[1]); + n = read(fds[0], err, sizeof err - 1); + close(fds[0]); + if (n < 0) + n = 0; + err[n] = '\0'; + assert(wait_aborted(pid)); + assert(strstr(err, "missing signal missing") != NULL); +} + +static void missing_property_int(void) { + SzString *name = sz_string_from_cstr("missing"); + (void)sz_property_signal_int(name); +} + +static void missing_property_str(void) { + SzString *name = sz_string_from_cstr("missing"); + (void)sz_property_signal_str(name); +} + +static void missing_property_list_len(void) { + SzString *name = sz_string_from_cstr("missing"); + (void)sz_property_signal_list_len(name); +} + +static void missing_timeline_int(void) { + FILE *f = fopen("/tmp/scuzz_ui_tl_missing.dump", "w"); + void *tl; + SzString *name; + assert(f); + fputs("# timeline v=1 n=1\n--- 0\nlast_hit:\n\ndrive:\n\nsignals:\n" + "int[0] count = 7\na11y:\n\n", + f); + fclose(f); + tl = sz_timeline_load("/tmp/scuzz_ui_tl_missing.dump"); + name = sz_string_from_cstr("missing"); + (void)sz_timeline_signal_int(tl, 0, name); +} + +static void test_property_missing_signal_panics(void) { + assert_missing_aborts(missing_property_int); + assert_missing_aborts(missing_property_str); + assert_missing_aborts(missing_property_list_len); + assert_missing_aborts(missing_timeline_int); +} + +static void test_signal_list_get_survives_set(void) { + SzSignalList *items; + SzList *old; + SzList *got; + SzList *neu; + SzString *a; + SzString *b; + + a = sz_string_from_cstr("a"); + old = sz_list_cons(a, sz_list_nil()); + sz_string_free(a); + items = sz_signal_list(old); + sz_release(old); + got = sz_lang_signal_list_get(items); + b = sz_string_from_cstr("b"); + neu = sz_list_cons(b, sz_list_nil()); + sz_string_free(b); + sz_signal_list_set(items, neu); + sz_release(neu); + assert(sz_list_len(got) == 1); + assert(got && got->head); + assert(strcmp(sz_string_cstr((SzString *)got->head), "a") == 0); + sz_release(got); + sz_signal_list_free(items); } static void test_property_signal_str(void) { @@ -13332,11 +13431,6 @@ static void test_property_signal_str(void) { assert(strcmp(sz_string_cstr(got), "oat") == 0); sz_string_free(got); sz_release(name); - name = sz_string_from_cstr("missing"); - got = sz_property_signal_str(name); - assert(strcmp(sz_string_cstr(got), "") == 0); - sz_string_free(got); - sz_release(name); sz_string_free(dump); sz_signal_str_free(draft); } @@ -15349,6 +15443,8 @@ int main(void) { test_property_signal_list_len(); test_property_signal_list_at(); test_property_signal_int(); + test_property_missing_signal_panics(); + test_signal_list_get_survives_set(); test_signal_list_record_dump(); test_property_signal_str(); test_signal_dump_escapes(); diff --git a/docs/gaps.md b/docs/gaps.md index 07128fe2..4e2042b7 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -49,7 +49,7 @@ These gaps keep the distinctive claims kernel-shaped. Close them in this order. Needed before a real CLI, server, or desktop app stays. -- **HTTP as a server** — Client kits return a body on 2xx (1 MiB cap). Live serve is localhost plaintext. Handler is `(path, method, body) => String`. Status, headers, and `0.0.0.0` bind stay out. HTTPS `Net.serve` stays out. POSIX sockets stay inside the runtime. Expand `Net` on this HTTP/1.0 stack. Do not add a second client. +- **HTTP as a server** — Client kits return a body on 2xx (1 MiB cap). Live serve is localhost plaintext. Handler is `(path, method, body) => IO[String]`. Status, headers, and `0.0.0.0` bind stay out. HTTPS `Net.serve` stays out. POSIX sockets stay inside the runtime. Expand `Net` on this HTTP/1.0 stack. Do not add a second client. - **Missing kits** — No calendar time, regex, hash, hex/base64, or UUID. `Map` / `Set` keys are `Int` or `String`. Expand blessed kits. No user FFI. - **`scuzz eval`** — No worksheet. A one-file eval helps humans and agents try one def. - **Kit docs** — No `scuzz doc`. Hover must show the same text as generated kit docs. diff --git a/docs/guide.md b/docs/guide.md index a504147f..53d4718f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -51,7 +51,7 @@ Console kit: `Sys.args(): IO[List[String]]`, `Sys.readLine(): IO[String]` (EOF - 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.*` (`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` +- 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.*` (`Ref.of` infers `A`; `Ref.update` / `Ref.updateAndGet`; `Ref` stays an open prefix), `Queue.unbounded` / `offer` / `take` (`Queue.take` returns `IO[A]` when the handle is `Queue[A]`; pin with `: IO[Queue[Int]]`; `offer` payload is `A`), `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`. 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. @@ -72,7 +72,7 @@ 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 `List[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. A non-empty list takes the key kind from the first cell. `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]` with `: IO[Queue[Int]]`. A missing pin is String. `Deferred.get` returns `IO[A]` when the handle is `Deferred[A]`. `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. The output key kind comes from the mapped key. `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.fromBool(b)` is `"true"` or `"false"`. `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 code-point width `n`. Fill cycles complete code points. `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: