From b92e27c776a9c745ec300eac856283b8648b77c8 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 5 Sep 2026 14:59:22 -0400 Subject: [PATCH 01/11] Emit a two-arg neth wrapper for Net.serve so a capturing lambda is a real SzCont instead of a one-arg user def or null. --- docs/gaps.md | 2 +- examples/compiler/src/Check.scuzz | 34 ++++++++++++++++++++- examples/compiler/src/Emit.scuzz | 51 ++++++------------------------- examples/io/src/Server.scuzz | 13 ++++---- examples/tyck/src/Main.scuzz | 8 ++++- 5 files changed, 58 insertions(+), 50 deletions(-) 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/examples/compiler/src/Check.scuzz b/examples/compiler/src/Check.scuzz index 220ee07a..064b0bbc 100644 --- a/examples/compiler/src/Check.scuzz +++ b/examples/compiler/src/Check.scuzz @@ -468,7 +468,7 @@ def resolveCall2(f: String, args: List[Expr], env: List[(String, String)], funs: if (isKit(f) || isOpenKit(f)) resolveKit(f, args, env, funs, ens, span) else if (hasEn(ens, f)) inferCtor(f, args, env, funs, ens) else if (enOfCase(ens, f) != "") inferCtor(enOfCase(ens, f), args, env, funs, ens) else resolveFunOrUser(f, args, env, funs, ens, span, lookupEnv(env, f)) def resolveKit(f: String, args: List[Expr], env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = - if (f == "Resource.make" || f == "Resource.use") resolveResource(f, args, env, funs, ens, span) else if (List.isEmpty(args) && isKit(f) && !List.isEmpty(kitParams(f))) ok(funTyFrom(kitParams(f), kitRet(f))) else if (isKit(f)) checkKnown(f, kitParams(f), kitRet(f), args, env, funs, ens, span) else checkKnown(f, anyParams(List.len(args)), openKitRet(f), args, env, funs, ens, span) + if (f == "Resource.make" || f == "Resource.use") resolveResource(f, args, env, funs, ens, span) else if (isNetServe(f)) resolveNetServe(f, args, env, funs, ens, span) else if (List.isEmpty(args) && isKit(f) && !List.isEmpty(kitParams(f))) ok(funTyFrom(kitParams(f), kitRet(f))) else if (isKit(f)) checkKnown(f, kitParams(f), kitRet(f), args, env, funs, ens, span) else checkKnown(f, anyParams(List.len(args)), openKitRet(f), args, env, funs, ens, span) def enOfCase(ens: List[En], ctor: String): String = if (List.isEmpty(ens)) "" else enOfCase1(List.at(ens, 0), List.tail(ens), ctor) @@ -547,6 +547,38 @@ def bindOfAnn(a: Out): String = def resolveResBody(recv: String, body: Out, span: (String, Int), makeRet: String): Out = if (hasErr(body)) body else if (isIo(body.ty) || body.ty == "Any") ok(if (makeRet != "") makeRet else if (isIo(body.ty)) body.ty else "IO[Any]") else bad(Str.concat(if (makeRet == "") "Resource.use" else "Resource.make", Str.concat(" needs IO, got ", Str.concat(recv, Str.concat(" and ", body.ty)))), span) +def netReqTy(): String = + "(String, String, String)" + +def isIoStr(t: String): Bool = + isIo(t) && tyEq(ioOk(t), "String") + +def netFunOk(ty: String): Bool = + isFunTy(ty) && tyEq(funArgOf(ty), netReqTy()) && isIoStr(funRetOf(ty)) + +def resolveNetServe(f: String, args: List[Expr], env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = + if (List.isEmpty(args)) ok(funTyFrom(kitParams(f), kitRet(f))) else if (List.len(args) != 2) bad(Str.concat(f, Str.concat(" expects 2 args, got ", Str.fromInt(List.len(args)))), span) else resolveNetServe2(f, infer(stripNamed(List.at(args, 0)), env, funs, ens), List.at(args, 1), env, funs, ens, span) + +def resolveNetServe2(f: String, po: Out, lam: Expr, env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = + if (hasErr(po)) po else if (tyEq(po.ty, "Int")) resolveNetLam(f, lam, env, funs, ens, span) else argMismatch(f, "Int", po.ty, span) + +def resolveNetLam(f: String, lam: Expr, env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = + lam match { + case Expr.ELam(p, ty, body) => resolveNetAnn(annOk(ty, netReqTy(), span), f, p, body, env, funs, ens, span) + case Expr.ENamed(_, inner) => resolveNetLam(f, inner, env, funs, ens, span) + case Expr.EAscribe(inner, _, _) => resolveNetLam(f, inner, env, funs, ens, span) + case _ => resolveNetFun(f, infer(stripNamed(lam), env, funs, ens), span) + } + +def resolveNetAnn(a: Out, f: String, p: String, body: Expr, env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = + if (hasErr(a)) a else resolveNetBody(f, infer(body, bindRes(p, bindOfAnn(a), env), funs, ens), span) + +def resolveNetBody(f: String, body: Out, span: (String, Int)): Out = + if (hasErr(body)) body else if (isIoStr(body.ty) || body.ty == "Any") ok("IO[Unit]") else bad(Str.concat(f, Str.concat(" needs IO[String], got ", body.ty)), span) + +def resolveNetFun(f: String, o: Out, span: (String, Int)): Out = + if (hasErr(o)) o else if (netFunOk(o.ty)) ok("IO[Unit]") else bad(Str.concat(f, " needs a lambda"), span) + def openKitBool(f: String): Bool = f == "List.exists" || f == "List.forall" || f == "List.contains" || f == "List.nonEmpty" || f == "List.isEmpty" diff --git a/examples/compiler/src/Emit.scuzz b/examples/compiler/src/Emit.scuzz index 7db97ad5..878385cb 100644 --- a/examples/compiler/src/Emit.scuzz +++ b/examples/compiler/src/Emit.scuzz @@ -1029,54 +1029,23 @@ def emitSometimes(code: String, vals: List[String], owns: List[Bool], _prefix: S Slot(join(code, join(line(Str.concat("call void @sz_property_sometimes(ptr ", Str.concat(arg0(vals), ")"))), if (headOwn(owns)) relPtr(arg0(vals)) else "")), "null", false) def emitNetServe(f: String, args: List[Expr], prefix: String, strs: List[String], defs: List[Fun], ens: List[En], ps: List[Param], loc: String): Slot = - emitNetServe2(if (f == "Net.serve") "sz_net_serve" else "sz_net_serve_once", emitExpr(List.at(args, 0), Str.concat(prefix, "_p"), strs, defs, ens, ps, loc), if (List.isEmpty(List.tail(args))) Expr.EUnit else List.at(List.tail(args), 0), prefix, defs, modOfLoc(loc)) + emitNetServe2(if (f == "Net.serve") "sz_net_serve" else "sz_net_serve_once", emitExpr(List.at(args, 0), Str.concat(prefix, "_p"), strs, defs, ens, ps, loc), wrapPh(if (List.isEmpty(List.tail(args))) Expr.EUnit else List.at(List.tail(args), 0), ps, defs, modOfLoc(loc)), prefix, strs, defs, ens, ps, loc) -def emitNetServe2(rt: String, port: Slot, fn: Expr, prefix: String, defs: List[Fun], mod: String): Slot = +def emitNetServe2(rt: String, port: Slot, lam: Expr, prefix: String, strs: List[String], defs: List[Fun], ens: List[En], ps: List[Param], loc: String): Slot = port match { - case Slot(pc, pv, po) => Slot(join(pc, join(if (po) relPtr(pv) else "", line(Str.concat(tmp(prefix, "io"), Str.concat("call ptr @", Str.concat(rt, Str.concat("(i64 ", Str.concat(pv, Str.concat(", ptr ", Str.concat(netServePtr(fn, defs, mod), ", ptr null)")))))))))), pct(prefix, "io"), false) + case Slot(pc, pv, po) => emitNetServe3(rt, pc, pv, po, mapLamBind(lam), mapLamBody(lam), prefix, strs, defs, ens, ps, loc) } -def netServePtr(e: Expr, defs: List[Fun], mod: String): String = - e match { - case Expr.ELam(p, _, body) => netServePtrLam(p, body, defs, mod) - case Expr.EVar(f, _) => funPtr(defs, f, mod) - case Expr.EMethod(recv, name, _, _) => methodPtr(recv, name) - case Expr.ECall(f, _, _) => funPtr(defs, f, mod) - case _ => "null" - } - -def netServePtrLam(p: String, body: Expr, defs: List[Fun], mod: String): String = - body match { - case Expr.ECall(f, args, _) => if (etaOne(p, args)) funPtr(defs, f, mod) else "null" - case Expr.EMethod(recv, name, args, _) => if (etaOne(p, args)) methodPtr(recv, name) else "null" - case _ => "null" - } +def emitNetServe3(rt: String, pc: String, pv: String, po: Bool, bind: String, body: Expr, prefix: String, strs: List[String], defs: List[Fun], ens: List[En], ps: List[Param], loc: String): Slot = + emitNetServe4(rt, pc, pv, po, emitNethGoTy(prefix, bind, body, dropParam(ps, bind), strs, defs, ens, loc, false, "(String, String, String)"), emitPackEnv(ps, Str.concat(prefix, "_e")), prefix) -def etaOne(p: String, args: List[Expr]): Bool = - !List.isEmpty(args) && List.isEmpty(List.tail(args)) && isVarName(List.at(args, 0), p) - -def isVarName(e: Expr, n: String): Bool = - e match { - case Expr.EVar(s, _) => s == n - case _ => false - } - -def funPtr(defs: List[Fun], f: String, mod: String): String = - funPtr2(findDefPrefer(defs, f, mod), f) - -def funPtr2(hit: List[Fun], f: String): String = - Str.concat("@sz_user_", if (List.isEmpty(hit)) Str.concat("Main_", f) else symOfHit(List.at(hit, 0))) - -def symOfHit(d: Fun): String = - d match { - case Fun(_, name, _, _, _, _, mod, _) => symOf(mod, name) +def emitNetServe4(rt: String, pc: String, pv: String, po: Bool, neth: String, env: Slot, prefix: String): Slot = + env match { + case Slot(ec, ev, eo) => Slot(join(neth, join(pc, join(ec, join(line(netServeCall(rt, pv, prefix, ev)), join(if (po) relPtr(pv) else "", emitFmRel(ev, eo)))))), pct(prefix, "io"), false) } -def methodPtr(recv: Expr, name: String): String = - recv match { - case Expr.EVar(mod, _) => Str.concat("@sz_user_", Str.concat(if (mod == "") "Main" else mod, Str.concat("_", name))) - case _ => "null" - } +def netServeCall(rt: String, pv: String, prefix: String, ev: String): String = + Str.concat(tmp(prefix, "io"), Str.concat("call ptr @", Str.concat(rt, Str.concat("(i64 ", Str.concat(pv, Str.concat(", ptr @sz_neth_", Str.concat(prefix, Str.concat(", ptr ", Str.concat(ev, ")"))))))))) def isPredKit(f: String): Bool = f == "Stream.filter" || f == "Stream.takeWhile" || f == "Stream.dropWhile" || f == "Stream.find" || f == "Stream.exists" || f == "Stream.filterNot" || f == "Stream.forall" || f == "Stream.none" || f == "Stream.findLast" || f == "List.filter" || f == "List.filterNot" || f == "List.takeWhile" || f == "List.dropWhile" || f == "List.partition" || f == "Map.filter" || f == "Set.filter" || f == "List.find" || f == "List.findLast" || f == "List.span" || f == "Verdict.every" || f == "Verdict.any" diff --git a/examples/io/src/Server.scuzz b/examples/io/src/Server.scuzz index 7be88ea4..d59b5860 100644 --- a/examples/io/src/Server.scuzz +++ b/examples/io/src/Server.scuzz @@ -1,15 +1,16 @@ -def handle(req: (String, String, String)): IO[String] = - req match { - case (path, method, body) => IO.println(s"served:$method:$path:$body").map(_ => s"ok:$path") - } - def showPing(p: (Unit, String)): IO[Unit] = p match { case (_, body) => IO.println(s"ping:$body") } def runHttp(): IO[Unit] = - IO.both(Net.serveOnce(8080, handle), Net.httpPost("http://127.0.0.1:8080/ping", "hi")).flatMap(p => showPing(p)) + for { + tag = "ok" + p <- IO.both(Net.serveOnce(8080, req => req match { + case (path, method, body) => IO.println(s"served:$method:$path:$body").map(_ => s"$tag:$path") +}), Net.httpPost("http://127.0.0.1:8080/ping", "hi")) + _ <- showPing(p) + } yield () def echoServer(ln: Any): IO[Unit] = for { diff --git a/examples/tyck/src/Main.scuzz b/examples/tyck/src/Main.scuzz index f64e6a23..5c77842b 100644 --- a/examples/tyck/src/Main.scuzz +++ b/examples/tyck/src/Main.scuzz @@ -192,6 +192,12 @@ def srcDefMis(): String = def wantDefMis(): String = "[{\"severity\":\"error\",\"message\":\"type error: Deferred.complete arg type mismatch: expected Deferred, got String\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":21,\"end_line\":2,\"end_column\":27}]" +def srcNetStr(): String = + "@main def main: IO[Unit] =\n Net.serveOnce(8080, \"no\")\n" + +def wantNetStr(): String = + "[{\"severity\":\"error\",\"message\":\"type error: Net.serveOnce needs a lambda\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":3,\"end_line\":2,\"end_column\":16}]" + def srcJsonUnk(): String = """@main def main: IO[Unit] = Json.notAKit(1, 2, 3) @@ -290,7 +296,7 @@ def tyckFlow(): Bool = tyckDiff(srcUnBool(), wantUnBool()) && tyckDiff(srcIfMis(), wantIfMis()) && tyckDiff(srcMapHandle(), wantMapHandle()) && tyckDiff(srcJoinMis(), wantJoinMis()) && tyckDiff(srcNope(), wantNope()) && tyckDiff(srcStrUnk(), wantStrUnk()) && tyckDiff(srcStrConcatTy(), wantStrConcatTy()) && tyckDiff(srcSysExit(), wantSysExit()) def tyckRes(): Bool = - tyckDiff(srcResArity(), wantResArity()) && tyckDiff(srcResDraw(), wantResDraw()) && tyckDiff(srcResLam(), wantResLam()) && tyckDiff(srcResUnk(), wantResUnk()) && tyckDiff(srcJsonUnk(), wantJsonUnk()) && tyckDiff(srcDefUnk(), wantDefUnk()) && tyckDiff(srcDefMis(), wantDefMis()) + tyckDiff(srcResArity(), wantResArity()) && tyckDiff(srcResDraw(), wantResDraw()) && tyckDiff(srcResLam(), wantResLam()) && tyckDiff(srcResUnk(), wantResUnk()) && tyckDiff(srcJsonUnk(), wantJsonUnk()) && tyckDiff(srcDefUnk(), wantDefUnk()) && tyckDiff(srcDefMis(), wantDefMis()) && tyckDiff(srcNetStr(), wantNetStr()) def tyckFail(): Bool = tyckDiff(srcFailOk(), wantFailOk()) && tyckDiff(srcFailMis(), wantFailMis()) From a0dd692080846b0fe2dab669b9a572639030d7c8 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 5 Sep 2026 15:25:15 -0400 Subject: [PATCH 02/11] Define sz_json_is_float and coerce exact JSON numbers in intOr/floatOr so getInt on 1.0 and getFloat on 1 stop returning the default. --- crates/runtime/include/scuzz_rt.h | 6 ++++- crates/runtime/src/json.c | 43 +++++++++++++++++++------------ crates/runtime/tests/test_io.c | 36 ++++++++++++++++++++++++++ docs/guide.md | 2 +- examples/io/src/Query.scuzz | 28 ++++++++++++++++++++ 5 files changed, 96 insertions(+), 19 deletions(-) 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/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/tests/test_io.c b/crates/runtime/tests/test_io.c index ee755729..948966fa 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -4032,6 +4032,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; diff --git a/docs/guide.md b/docs/guide.md index a504147f..0f5b2cf8 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -149,6 +149,6 @@ count.scuzz_verify # Timeline => Verdict session claims and Bool drive oracl | `examples/studio` | Desktop stay-open app: `showWhen` pages, `Signal.list` + `View.each`, Done/Add/Del/Rename, `View.radio` / `View.slider` / `View.progress` / `View.switch` / `View.chip` / `View.filterChip` / `View.choiceChip` / `View.actionChip` / `View.inputChip` / `View.listTile` / `View.badge` / `View.card` / `View.divider` / `View.expansionTile` / `View.iconButton` / `View.verticalDivider` / `View.circularProgress` / `View.avatar` / `View.checkboxListTile` / `View.switchListTile` / `View.radioListTile` / `View.segmented` / `View.fab` / `View.outlinedButton` / `View.textButton` / `View.tooltip` / `View.placeholder` / `View.semantics` / `View.mergeSemantics` / `View.inkWell` / `View.visibility` / `View.offstage` / `View.unconstrainedBox` / `View.scrollH` / `View.grid`, Fs load/save, `record` / `trait` / stem modules, `*.scuzz_verify` + drivers / `Property.sometimes`. `scuzz run` opens a window (close the window to quit). `--headless` snapshots. | | `examples/kernel` | Language constructs: enums, `record` + `where` + `.copy`, `trait` / `impl`, generics, generic enum/record, type aliases, stem modules, `private def`, `import` / `as` / `*`, unused names, `Float`, match guards, literal match, or-patterns, as-patterns, list patterns (`[]` / `::` / `[a, b]`), named field patterns, bare constructors (`case None`, `Some(1)`), tuple of 2 through 8 slots, tuple and constructor `for` / lambda unpack, `if` in `for`, `if` without `else` (`Unit` / `IO[Unit]`), `IO.fail` as `IO[A]`, `io.map`, case lambdas (`{ case … }`), `A => B` apply (`f(x)`, `f(x, y)`), named Fun values (`inc = (_ + 1): Int => Int`, `addN`), unary-def eta (`Str.fromInt`, `id`), n-ary-def eta (`add`), cons `h :: t`, named call arguments, default arguments, type ascription, typed lambdas, placeholder lambdas (`_ + 1`), structural `==`, numeric separators, scientific floats, triple-quoted strings, self-tail calls (loop lowering), Builder (linear string kit), recursive `Term` drive (`termDiff`) | | `examples/scale` | Compiler-scale package: about 4k lines across stem modules. A live run fills a String-keyed `Map` of 3048 entries (`mapn:3048`, `hit:0:49`). `scuzz fuzz --iterations 8` samples 3 of 97 live-code sites and finishes in about 36 s | -| `examples/io` | Blessed kits: Clock / `Random.nextInt` / Fs (`list` entries, `exists`, `join` / `dirname` / `basename`, `delete` / `rename` / `walk`) / `Impurity.runKit` / `Ref` / `Queue` / `Deferred` (`empty` / `get` / `complete` / `fail`) / `Fiber` / `Resource` / `Stream` (range / zip / interleave / zipWith / flatMap / mapConcat / scan / fold / forall / iterate / unfold / head) / Json query (`get` / `keys` / `arr` / `merge` / `isBool` / `getFloat` / `asFloat`) and write (`set` / `remove` / `append` / `dropAt`); parse miss is Result.Err / `Net.serveOnce` POST through virtual loopback plus TCP echo and UDP ping (`Impurity.runKit` and serve under `scuzz test`). `flow.scuzz_verify` claims fiber census, effect count, checkpoint, and nearest checkpoint stay in range. `fs.scuzz_verify` claims a `Fs.write` effect on the timeline. `net.scuzz_verify` claims a `Net.httpPost` effect | +| `examples/io` | Blessed kits: Clock / `Random.nextInt` / Fs (`list` entries, `exists`, `join` / `dirname` / `basename`, `delete` / `rename` / `walk`) / `Impurity.runKit` / `Ref` / `Queue` / `Deferred` (`empty` / `get` / `complete` / `fail`) / `Fiber` / `Resource` / `Stream` (range / zip / interleave / zipWith / flatMap / mapConcat / scan / fold / forall / iterate / unfold / head) / Json query (`get` / `keys` / `arr` / `merge` / `isBool` / `isFloat` / `getFloat` / `asFloat`) and write (`set` / `remove` / `append` / `dropAt`); parse miss is Result.Err / `Net.serveOnce` POST through virtual loopback plus TCP echo and UDP ping (`Impurity.runKit` and serve under `scuzz test`). `flow.scuzz_verify` claims fiber census, effect count, checkpoint, and nearest checkpoint stay in range. `fs.scuzz_verify` claims a `Fs.write` effect on the timeline. `net.scuzz_verify` claims a `Net.httpPost` effect | Edit [vision.md](vision.md) when changing GC, Skia, effects, UI boundaries, or language direction. diff --git a/examples/io/src/Query.scuzz b/examples/io/src/Query.scuzz index 718c2d1a..c3c96587 100644 --- a/examples/io/src/Query.scuzz +++ b/examples/io/src/Query.scuzz @@ -46,6 +46,33 @@ private def printArrWrite(): IO[Unit] = private def printFloat(): IO[Unit] = IO.println(if (Json.floatOr(Json.Float(1.5), 0.0) > 0.0) "float:pos" else "float:z") +private def printIsFloat(src: String, label: String): IO[Unit] = + Json.parse(src) match { + case Result.Err(_) => IO.println(Str.concat(label, ":err")) + case Result.Ok(j) => IO.println(Str.concat(label, if (Json.isFloat(j)) ":1" else ":0")) + } + +private def printGetIntSrc(src: String, label: String): IO[Unit] = + Json.parse(src) match { + case Result.Err(_) => IO.println(Str.concat(label, ":err")) + case Result.Ok(j) => IO.println(s"$label:${Str.fromInt(Json.getInt(j, "n", -1))}") + } + +private def printGetFloatSrc(src: String, label: String): IO[Unit] = + Json.parse(src) match { + case Result.Err(_) => IO.println(Str.concat(label, ":err")) + case Result.Ok(j) => IO.println(if (Json.getFloat(j, "n", -1.0) > 0.5) Str.concat(label, ":pos") else Str.concat(label, ":z")) + } + +private def coerceKit(): IO[Unit] = + for { + _ <- printIsFloat("1.5", "isFloat15") + _ <- printIsFloat("1", "isFloat1") + _ <- printGetIntSrc("{\"n\":1.0}", "getInt10") + _ <- printGetIntSrc("{\"n\":1e2}", "getInt1e2") + _ <- printGetFloatSrc("{\"n\":1}", "getFloat1") + } yield () + private def printStr(j: Json, key: String): IO[Unit] = IO.println(Str.concat(s"str:$key:", Json.getStr(j, key, ""))) @@ -126,5 +153,6 @@ def run(): IO[Unit] = _ <- missKit() _ <- parseErr() _ <- printArrWrite() + _ <- coerceKit() } yield () From 12d1d41de35f9b749a84c8eb0194476471a740fd Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 5 Sep 2026 16:17:55 -0400 Subject: [PATCH 03/11] Keep take(n) as an inner item budget in transformer walkers and pull filter/find until remain matches so skipped items still count against take and a miss does not stop the pull. --- crates/runtime/src/stream.c | 369 ++++++++++++++++++++++++++++----- crates/runtime/tests/test_io.c | 89 ++++++++ 2 files changed, 411 insertions(+), 47 deletions(-) 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/tests/test_io.c b/crates/runtime/tests/test_io.c index 948966fa..f76ad72f 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)); @@ -4249,6 +4270,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())); From f83d20589c05507b74a6f3bc1816dab0accab28c Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 5 Sep 2026 17:01:03 -0400 Subject: [PATCH 04/11] Close Queue.unbounded, offer, and take as a named kit so Queue.foo is unknown and a String is not a Queue, and refine take/offer from Queue[A] so a String payload on Queue[Int] fails check. --- docs/guide.md | 2 +- examples/compiler/src/Check.scuzz | 28 ++++++++++++++++++++-------- examples/tyck/src/Main.scuzz | 28 +++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 0f5b2cf8..60ebb48c 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. diff --git a/examples/compiler/src/Check.scuzz b/examples/compiler/src/Check.scuzz index 064b0bbc..b660fa54 100644 --- a/examples/compiler/src/Check.scuzz +++ b/examples/compiler/src/Check.scuzz @@ -255,7 +255,7 @@ def isKitF(f: String): Bool = isNetHttp(f) || isNetServe(f) || isNetTcp(f) || isNetUdp(f) def isKitG(f: String): Bool = - f == "Timeline.signalInt" || f == "Timeline.signalListLen" || f == "Timeline.signalStrHas" || f == "Property.signalInt" || f == "Property.signalStr" || f == "Property.signalListLen" || f == "Property.signalListAt" || f == "Verdict.alwaysHas" || f == "Verdict.afterHit" || f == "Resource.make" || f == "Resource.use" || f == "Deferred.empty" || f == "Deferred.get" || f == "Deferred.complete" || f == "Deferred.fail" + f == "Timeline.signalInt" || f == "Timeline.signalListLen" || f == "Timeline.signalStrHas" || f == "Property.signalInt" || f == "Property.signalStr" || f == "Property.signalListLen" || f == "Property.signalListAt" || f == "Verdict.alwaysHas" || f == "Verdict.afterHit" || f == "Resource.make" || f == "Resource.use" || f == "Deferred.empty" || f == "Deferred.get" || f == "Deferred.complete" || f == "Deferred.fail" || f == "Queue.unbounded" || f == "Queue.offer" || f == "Queue.take" def isNetHttp(f: String): Bool = f == "Net.httpGet" || f == "Net.httpPost" || f == "Net.httpPut" || f == "Net.httpPatch" || f == "Net.httpDelete" || f == "Net.httpHead" @@ -282,7 +282,7 @@ def kitParams3(f: String): List[String] = if (f == "Net.httpGet" || f == "Net.httpDelete" || f == "Net.httpHead") "String" :: noStr() else if (f == "Net.httpPost" || f == "Net.httpPut" || f == "Net.httpPatch") "String" :: "String" :: noStr() else if (f == "Net.serve" || f == "Net.serveOnce") "Int" :: "Any" :: noStr() else if (f == "Net.tcpConnect") "String" :: "Int" :: noStr() else if (f == "Net.tcpListen" || f == "Net.udpBind") "Int" :: noStr() else if (f == "Net.tcpAccept" || f == "Net.tcpClose" || f == "Net.udpClose") "Any" :: noStr() else if (f == "Net.tcpRead" || f == "Net.udpRecv") "Any" :: "Int" :: noStr() else if (f == "Net.tcpWrite") "Any" :: "String" :: noStr() else if (f == "Net.udpSend") "Any" :: "String" :: "Int" :: "String" :: noStr() else kitParams4(f) def kitParams4(f: String): List[String] = - if (f == "Timeline.signalInt" || f == "Timeline.signalListLen") "Timeline" :: "Int" :: "String" :: noStr() else if (f == "Timeline.signalStrHas") "Timeline" :: "Int" :: "String" :: "String" :: noStr() else if (f == "Property.signalInt" || f == "Property.signalStr" || f == "Property.signalListLen") "String" :: noStr() else if (f == "Property.signalListAt") "String" :: "Int" :: noStr() else if (f == "Verdict.alwaysHas") "Timeline" :: "String" :: noStr() else if (f == "Verdict.afterHit") "Timeline" :: "String" :: "String" :: noStr() else if (f == "Resource.make") "IO" :: "Any" :: noStr() else if (f == "Resource.use") "Resource" :: "Any" :: noStr() else if (f == "Deferred.empty") noStr() else if (f == "Deferred.get") "Deferred" :: noStr() else if (f == "Deferred.complete") "Deferred" :: "Any" :: noStr() else if (f == "Deferred.fail") "Deferred" :: "String" :: noStr() else noStr() + if (f == "Timeline.signalInt" || f == "Timeline.signalListLen") "Timeline" :: "Int" :: "String" :: noStr() else if (f == "Timeline.signalStrHas") "Timeline" :: "Int" :: "String" :: "String" :: noStr() else if (f == "Property.signalInt" || f == "Property.signalStr" || f == "Property.signalListLen") "String" :: noStr() else if (f == "Property.signalListAt") "String" :: "Int" :: noStr() else if (f == "Verdict.alwaysHas") "Timeline" :: "String" :: noStr() else if (f == "Verdict.afterHit") "Timeline" :: "String" :: "String" :: noStr() else if (f == "Resource.make") "IO" :: "Any" :: noStr() else if (f == "Resource.use") "Resource" :: "Any" :: noStr() else if (f == "Deferred.empty") noStr() else if (f == "Deferred.get") "Deferred" :: noStr() else if (f == "Deferred.complete") "Deferred" :: "Any" :: noStr() else if (f == "Deferred.fail") "Deferred" :: "String" :: noStr() else if (f == "Queue.unbounded") noStr() else if (f == "Queue.offer") "Queue" :: "Any" :: noStr() else if (f == "Queue.take") "Queue" :: noStr() else noStr() def kitNames(): List[String] = List.concat(kitNamesA(), List.concat(kitNamesB(), List.concat(kitNamesC(), List.concat(kitNamesD(), List.concat(kitNamesE(), List.concat(kitNamesF(), kitNamesG())))))) @@ -306,7 +306,7 @@ def kitNamesF(): List[String] = "Net.httpGet" :: "Net.httpPost" :: "Net.httpPut" :: "Net.httpPatch" :: "Net.httpDelete" :: "Net.httpHead" :: "Net.serve" :: "Net.serveOnce" :: "Net.tcpConnect" :: "Net.tcpListen" :: "Net.tcpAccept" :: "Net.tcpRead" :: "Net.tcpWrite" :: "Net.tcpClose" :: "Net.udpBind" :: "Net.udpSend" :: "Net.udpRecv" :: "Net.udpClose" :: noStr() def kitNamesG(): List[String] = - "Timeline.signalInt" :: "Timeline.signalListLen" :: "Timeline.signalStrHas" :: "Property.signalInt" :: "Property.signalStr" :: "Property.signalListLen" :: "Property.signalListAt" :: "Verdict.alwaysHas" :: "Verdict.afterHit" :: "Resource.make" :: "Resource.use" :: "Deferred.empty" :: "Deferred.get" :: "Deferred.complete" :: "Deferred.fail" :: noStr() + "Timeline.signalInt" :: "Timeline.signalListLen" :: "Timeline.signalStrHas" :: "Property.signalInt" :: "Property.signalStr" :: "Property.signalListLen" :: "Property.signalListAt" :: "Verdict.alwaysHas" :: "Verdict.afterHit" :: "Resource.make" :: "Resource.use" :: "Deferred.empty" :: "Deferred.get" :: "Deferred.complete" :: "Deferred.fail" :: "Queue.unbounded" :: "Queue.offer" :: "Queue.take" :: noStr() def kitRet(f: String): String = if (f == "Timeline.signalInt" || f == "Timeline.signalListLen" || f == "Property.signalInt" || f == "Property.signalListLen") "Int" else if (f == "Timeline.signalStrHas") "Bool" else if (f == "Property.signalStr" || f == "Property.signalListAt" || f == "List.join" || f == "Json.getStr" || f == "Json.strOr") "String" else if (f == "Verdict.alwaysHas" || f == "Verdict.afterHit") "Verdict" else if (f == "Str.len" || f == "Str.byteLen" || f == "List.len" || f == "Str.charAt" || f == "Str.toInt" || f == "Str.indexOf" || f == "Str.lastIndexOf" || f == "Map.size" || f == "Set.size" || f == "Json.getInt" || f == "Json.intOr") "Int" else if (f == "List.head" || f == "Map.get") "Option" else if (f == "List.at" || f == "Map.getOrElse") "Any" else if (f == "List.isEmpty" || f == "Map.contains" || f == "Set.contains" || f == "Json.has" || f == "Json.getBool" || f == "Json.boolOr" || f == "Json.isNull" || f == "Json.isObj" || f == "Json.isArr" || f == "Json.isBool" || f == "Json.isInt" || f == "Json.isStr" || f == "Json.isFloat" || f == "Str.startsWith" || f == "Str.endsWith" || f == "Str.contains" || f == "Str.eq" || f == "Str.isEmpty" || f == "Str.nonEmpty" || f == "Str.isBlank") "Bool" else if (f == "List.concat" || f == "List.reverse" || f == "List.tail" || f == "List.cons" || f == "List.take" || f == "List.drop" || f == "List.takeRight" || f == "List.dropRight" || f == "List.init" || f == "List.last" || f == "List.flatten" || f == "Str.lines" || f == "Str.split") "List" else if (f == "Json.floatOr" || f == "Json.Float" || f == "Json.getFloat") "Float" else if (f == "Map.keys" || f == "Map.values" || f == "Map.toList" || f == "Set.toList" || f == "Json.keys" || f == "Json.arr" || f == "Json.at" || f == "Json.pairs" || f == "Json.asInt" || f == "Json.asBool" || f == "Json.asStr" || f == "Json.asFloat") "List" else if (f == "Builder.empty" || f == "Builder.append") "Builder" else if (f == "Fs.read" || f == "Sys.getenv" || f == "Sys.read" || f == "Sys.readLine" || f == "Fs.canonicalize" || f == "Sys.childRead" || isNetHttp(f) || f == "Net.tcpRead") "IO[String]" else if (f == "Fs.write" || f == "Fs.mkdirs" || f == "Fs.delete" || f == "Fs.rename" || f == "Impurity.runKit" || f == "Sys.write" || f == "Sys.kill" || f == "Sys.childWrite" || f == "Sys.childClose" || f == "IO.println" || isNetServe(f) || f == "Net.tcpWrite" || f == "Net.tcpClose" || f == "Net.udpSend" || f == "Net.udpClose") "IO[Unit]" else if (f == "Fs.list" || f == "Fs.walk") "IO[List[(String, Bool)]]" else if (f == "Sys.args") "IO[List[String]]" else if (f == "Sys.spawn" || f == "Sys.alive" || f == "Fs.exists" || f == "Random.nextInt") "IO[Int]" else if (isSysProc(f)) "IO[(Int, String, String)]" else if (f == "Net.udpRecv") "IO[(String, Int, String)]" else if (f == "Net.tcpConnect" || f == "Net.tcpListen" || f == "Net.tcpAccept" || f == "Net.udpBind") "IO[Any]" else if (f == "IO.pure") "IO[Any]" else if (f == "Clock.monotonic" || f == "Clock.realTime") "IO[Int]" else if (f == "Map.empty" || f == "Map.set" || f == "Map.remove") "Map" else if (f == "Set.empty" || f == "Set.add" || f == "Set.remove" || f == "Set.union" || f == "Set.intersect" || f == "Set.diff") "Set" else if (f == "Json.parse" || f == "Json.stringify") "Result" else if (f == "Json.get" || f == "Json.set" || f == "Json.remove" || f == "Json.append" || f == "Json.prepend" || f == "Json.setAt" || f == "Json.dropAt" || f == "Json.merge" || f == "Json.Int" || f == "Json.Null" || f == "Json.Float" || f == "Json.Obj" || f == "Json.Str" || f == "Json.Bool" || f == "Json.Arr") "Any" else strKitRet(f) @@ -358,7 +358,13 @@ def zipCheckErr(callee: String, w: String, ty: String, err: String, sp: (String, if (Str.len(err) > 0) Out(ty, err, sp) else zipCheckEq(callee, w, ty, want, args, env, funs, ens, span, acc, asp) def zipCheckEq(callee: String, w: String, got: String, want: List[String], args: List[Expr], env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int), acc: String, asp: (String, Int)): Out = - if (tyEq(got, w) || w == "Any") zipCheck(callee, want, args, env, funs, ens, span, if (acc == "") got else acc) else argMismatch(callee, w, got, asp) + if (tyEq(got, w) || w == "Any") zipCheck(callee, kitWantFromTy(callee, if (acc == "") got else acc, want), args, env, funs, ens, span, if (acc == "") got else acc) else argMismatch(callee, w, got, asp) + +def kitWantFromTy(f: String, handleTy: String, want: List[String]): List[String] = + if (List.isEmpty(want)) want else if (f == "Queue.offer" || f == "Deferred.complete") offerElem(handleTy) :: List.tail(want) else want + +def offerElem(handleTy: String): String = + if (Str.startsWith(handleTy, "Queue[") || Str.startsWith(handleTy, "Deferred[")) elemOf(handleTy) else "Any" def argMismatch(callee: String, w: String, got: String, span: (String, Int)): Out = bad(argMismatchMsg(argMismatchWant(callee, w), got), span) @@ -438,11 +444,14 @@ def checkKnownRet(f: String, o: Out, ret: String): Out = } def kitRetFromTy(f: String, ty: String, ret: String): String = - if (f == "List.head") kitRetOption(ty) else if (f == "Map.get") kitRetMapOption(ty) else if (f == "List.at") elemOf(ty) else if (f == "List.tail" || f == "List.reverse" || f == "List.concat" || f == "List.last" || f == "List.find" || f == "List.findLast") kitRetList(ty, ret) else if (f == "List.cons") kitRetCons(ty, ret) else if (f == "IO.pure") ioTy("Any", ty) else if (f == "IO.fail") Str.concat("IO[", Str.concat(if (ty == "") "String" else ty, ", Any]")) else if (f == "Deferred.get") defGetRet(ty) else ret + if (f == "List.head") kitRetOption(ty) else if (f == "Map.get") kitRetMapOption(ty) else if (f == "List.at") elemOf(ty) else if (f == "List.tail" || f == "List.reverse" || f == "List.concat" || f == "List.last" || f == "List.find" || f == "List.findLast") kitRetList(ty, ret) else if (f == "List.cons") kitRetCons(ty, ret) else if (f == "IO.pure") ioTy("Any", ty) else if (f == "IO.fail") Str.concat("IO[", Str.concat(if (ty == "") "String" else ty, ", Any]")) else if (f == "Deferred.get") defGetRet(ty) else if (f == "Queue.take") queueTakeRet(ty) else ret def defGetRet(ty: String): String = if (Str.startsWith(ty, "Deferred[")) ioTy("Any", elemOf(ty)) else "IO[Any]" +def queueTakeRet(ty: String): String = + if (Str.startsWith(ty, "Queue[")) ioTy("Any", elemOf(ty)) else "IO[Any]" + def kitRetOption(ty: String): String = Str.concat("Option[", Str.concat(elemOf(ty), "]")) @@ -487,13 +496,13 @@ def hasCase1(c: EnCase, rest: List[EnCase], ctor: String): Bool = } def isOpenKit(f: String): Bool = - Str.startsWith(f, "List.") || Str.startsWith(f, "Map.") || Str.startsWith(f, "Set.") || Str.startsWith(f, "IO.") || Str.startsWith(f, "Float.") || Str.startsWith(f, "Signal.") || Str.startsWith(f, "Property.") || Str.startsWith(f, "View.") || Str.startsWith(f, "Ui.") || Str.startsWith(f, "Fiber.") || Str.startsWith(f, "Timeline.") || Str.startsWith(f, "Verdict.") || Str.startsWith(f, "Color.") || Str.startsWith(f, "Theme.") || Str.startsWith(f, "Ref.") || Str.startsWith(f, "Queue.") || Str.startsWith(f, "Stream.") || Str.startsWith(f, "Builder.") || Str.startsWith(f, "Oracle.") || Str.startsWith(f, ".") && f != ".apply" + Str.startsWith(f, "List.") || Str.startsWith(f, "Map.") || Str.startsWith(f, "Set.") || Str.startsWith(f, "IO.") || Str.startsWith(f, "Float.") || Str.startsWith(f, "Signal.") || Str.startsWith(f, "Property.") || Str.startsWith(f, "View.") || Str.startsWith(f, "Ui.") || Str.startsWith(f, "Fiber.") || Str.startsWith(f, "Timeline.") || Str.startsWith(f, "Verdict.") || Str.startsWith(f, "Color.") || Str.startsWith(f, "Theme.") || Str.startsWith(f, "Ref.") || Str.startsWith(f, "Stream.") || Str.startsWith(f, "Builder.") || Str.startsWith(f, "Oracle.") || Str.startsWith(f, ".") && f != ".apply" def anyParams(n: Int): List[String] = if (n <= 0) noStr() else "Any" :: anyParams(n - 1) def openKitRet(f: String): String = - if (openKitBool(f)) "Bool" else if (openKitInt(f) || Str.startsWith(f, "Color.") || Str.startsWith(f, "Theme.")) "Int" else if (f == "Property.sometimes") "Unit" else if (openKitAny(f) || Str.startsWith(f, "Signal.") || Str.startsWith(f, "View.") || Str.startsWith(f, "Property.")) "Any" else if (Str.startsWith(f, "Map.")) "Map" else if (Str.startsWith(f, "Set.")) "Set" else if (Str.startsWith(f, "Stream.")) streamKitRet(f) else if (Str.startsWith(f, "IO.") || Str.startsWith(f, "Ui.") || Str.startsWith(f, "Fiber.") || Str.startsWith(f, "Ref.") || Str.startsWith(f, "Queue.")) "IO[Any]" else if (Str.startsWith(f, "Str.")) "String" else if (Str.startsWith(f, "Float.")) "Float" else if (Str.startsWith(f, "Builder.") || Str.startsWith(f, "Oracle.") || Str.startsWith(f, ".")) "Any" else "Any" + if (openKitBool(f)) "Bool" else if (openKitInt(f) || Str.startsWith(f, "Color.") || Str.startsWith(f, "Theme.")) "Int" else if (f == "Property.sometimes") "Unit" else if (openKitAny(f) || Str.startsWith(f, "Signal.") || Str.startsWith(f, "View.") || Str.startsWith(f, "Property.")) "Any" else if (Str.startsWith(f, "Map.")) "Map" else if (Str.startsWith(f, "Set.")) "Set" else if (Str.startsWith(f, "Stream.")) streamKitRet(f) else if (Str.startsWith(f, "IO.") || Str.startsWith(f, "Ui.") || Str.startsWith(f, "Fiber.") || Str.startsWith(f, "Ref.")) "IO[Any]" else if (Str.startsWith(f, "Str.")) "String" else if (Str.startsWith(f, "Float.")) "Float" else if (Str.startsWith(f, "Builder.") || Str.startsWith(f, "Oracle.") || Str.startsWith(f, ".")) "Any" else "Any" def streamKitRet(f: String): String = if (f == "Stream.exists" || f == "Stream.forall" || f == "Stream.none") "IO[Bool]" else if (f == "Stream.count") "IO[Int]" else if (f == "Stream.compileToList" || f == "Stream.drain" || f == "Stream.head" || f == "Stream.last" || f == "Stream.fold") "IO[Any]" else "Stream" @@ -505,7 +514,10 @@ def resourceKitRet(f: String): String = if (f == "Resource.make") "Resource" else if (f == "Resource.use") "IO[Any]" else deferredKitRet(f) def deferredKitRet(f: String): String = - if (f == "Deferred.empty") "IO[Deferred]" else if (f == "Deferred.get") "IO[Any]" else if (f == "Deferred.complete" || f == "Deferred.fail") "IO[Unit]" else "Any" + if (f == "Deferred.empty") "IO[Deferred]" else if (f == "Deferred.get") "IO[Any]" else if (f == "Deferred.complete" || f == "Deferred.fail") "IO[Unit]" else queueKitRet(f) + +def queueKitRet(f: String): String = + if (f == "Queue.unbounded") "IO[Queue]" else if (f == "Queue.take") "IO[Any]" else if (f == "Queue.offer") "IO[Unit]" else "Any" def resolveResource(f: String, args: List[Expr], env: List[(String, String)], funs: List[Fun], ens: List[En], span: (String, Int)): Out = if (List.isEmpty(args)) ok(funTyFrom(kitParams(f), resourceKitRet(f))) else if (List.len(args) != 2) bad(Str.concat(f, Str.concat(" expects 2 args, got ", Str.fromInt(List.len(args)))), span) else resolveResource2(f, infer(stripNamed(List.at(args, 0)), env, funs, ens), List.at(args, 1), env, funs, ens, span) diff --git a/examples/tyck/src/Main.scuzz b/examples/tyck/src/Main.scuzz index 5c77842b..92e5d3ae 100644 --- a/examples/tyck/src/Main.scuzz +++ b/examples/tyck/src/Main.scuzz @@ -192,6 +192,32 @@ def srcDefMis(): String = def wantDefMis(): String = "[{\"severity\":\"error\",\"message\":\"type error: Deferred.complete arg type mismatch: expected Deferred, got String\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":21,\"end_line\":2,\"end_column\":27}]" +def srcQueUnk(): String = + """@main def main: IO[Unit] = + Queue.foo(1) +""" + +def wantQueUnk(): String = + "[{\"severity\":\"error\",\"message\":\"type error: unknown function Queue.foo\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":3,\"end_line\":2,\"end_column\":12}]" + +def srcQueMis(): String = + "@main def main: IO[Unit] =\n Queue.offer(\"nope\", 1)\n" + +def wantQueMis(): String = + "[{\"severity\":\"error\",\"message\":\"type error: Queue.offer arg type mismatch: expected Queue, got String\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":15,\"end_line\":2,\"end_column\":21}]" + +def srcQueTake(): String = + "@main def main: IO[Unit] =\n Queue.take(\"nope\")\n" + +def wantQueTake(): String = + "[{\"severity\":\"error\",\"message\":\"type error: Queue.take arg type mismatch: expected Queue, got String\",\"file\":\"Main.scuzz\",\"line\":2,\"column\":14,\"end_line\":2,\"end_column\":20}]" + +def srcQuePay(): String = + "@main def main: IO[Unit] =\n for {\n q <- Queue.unbounded(): IO[Queue[Int]]\n _ <- Queue.offer(q, \"x\")\n } yield ()\n" + +def wantQuePay(): String = + "[{\"severity\":\"error\",\"message\":\"type error: Queue.offer arg type mismatch: expected Int, got String\",\"file\":\"Main.scuzz\",\"line\":4,\"column\":25,\"end_line\":4,\"end_column\":28}]" + def srcNetStr(): String = "@main def main: IO[Unit] =\n Net.serveOnce(8080, \"no\")\n" @@ -296,7 +322,7 @@ def tyckFlow(): Bool = tyckDiff(srcUnBool(), wantUnBool()) && tyckDiff(srcIfMis(), wantIfMis()) && tyckDiff(srcMapHandle(), wantMapHandle()) && tyckDiff(srcJoinMis(), wantJoinMis()) && tyckDiff(srcNope(), wantNope()) && tyckDiff(srcStrUnk(), wantStrUnk()) && tyckDiff(srcStrConcatTy(), wantStrConcatTy()) && tyckDiff(srcSysExit(), wantSysExit()) def tyckRes(): Bool = - tyckDiff(srcResArity(), wantResArity()) && tyckDiff(srcResDraw(), wantResDraw()) && tyckDiff(srcResLam(), wantResLam()) && tyckDiff(srcResUnk(), wantResUnk()) && tyckDiff(srcJsonUnk(), wantJsonUnk()) && tyckDiff(srcDefUnk(), wantDefUnk()) && tyckDiff(srcDefMis(), wantDefMis()) && tyckDiff(srcNetStr(), wantNetStr()) + tyckDiff(srcResArity(), wantResArity()) && tyckDiff(srcResDraw(), wantResDraw()) && tyckDiff(srcResLam(), wantResLam()) && tyckDiff(srcResUnk(), wantResUnk()) && tyckDiff(srcJsonUnk(), wantJsonUnk()) && tyckDiff(srcDefUnk(), wantDefUnk()) && tyckDiff(srcDefMis(), wantDefMis()) && tyckDiff(srcQueUnk(), wantQueUnk()) && tyckDiff(srcQueMis(), wantQueMis()) && tyckDiff(srcQueTake(), wantQueTake()) && tyckDiff(srcQuePay(), wantQuePay()) && tyckDiff(srcNetStr(), wantNetStr()) def tyckFail(): Bool = tyckDiff(srcFailOk(), wantFailOk()) && tyckDiff(srcFailMis(), wantFailMis()) From 8258fd6c85a92200195bc6d080c867b40da57091 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 5 Sep 2026 17:46:11 -0400 Subject: [PATCH 05/11] Retain Signal.getList so setList cannot free a borrowed alias, and panic when a named Property or Timeline signal is missing so absence is not 0 or empty. --- crates/runtime/include/scuzz_ui.h | 2 + crates/runtime/src/signal.c | 70 +++++++++++------ crates/runtime/src/testrt.c | 30 +++++-- crates/runtime/tests/test_io.c | 5 +- crates/runtime/tests/test_ui.c | 126 ++++++++++++++++++++++++++---- docs/guide.md | 6 +- docs/vision.md | 4 +- examples/compiler/src/Emit.scuzz | 4 +- 8 files changed, 192 insertions(+), 55 deletions(-) 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/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/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/tests/test_io.c b/crates/runtime/tests/test_io.c index f76ad72f..df052ea6 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -11686,10 +11686,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..20e84268 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 @@ -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/guide.md b/docs/guide.md index 60ebb48c..53d4718f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -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: