I've just triaged and updated all the remaining issues in basic-cli https://github.com/roc-lang/basic-cli/issues
There are a few Good First Issues there if anyone is looking for ways to contribute, but maybe more comfortable with Rust or tasks with a smaller scope.
Thanks for the triage — I'd like to start picking these up. I've been working through the List/Str builtins on #9596, so basic-cli is a new repo for me, but I've got the platform oriented: Host.roc declaration → main.roc hosted entry → generated glue → lib.rs, with the public modules wrapping Host.*.
Starting with #409 (Sleep.seconds!) as a warm-up — purely additive Roc in Sleep.roc over the existing Host.sleep_millis!, so no host or glue changes.
One design question before I write it: seconds! : U64 => {}, consistent with the existing millis!, or F64 for fractional sleeps like 1.5? I lean U64 for consistency — fractional waits are already expressible via millis!. I'd also update examples/time.roc, which currently reads Sleep.millis!(1000) with a # 1000 ms = 1 second comment.
After that I'd look at #386 (Path.replace_utf8! — also pure Roc, and per the triage note it wants retitling, since whole-file ops now live on Path), then #109 (append), which would be my first pass through the Rust host.
On #223 and #162 — the triage notes suggest dropping good first issue given the cross-platform PATH/PATHEXT/OsStr and symlink/canonicalization surface. Agreed, but I'm interested in both once I've got a couple of the smaller ones through.
F64for fractional sleeps like1.5?
I like that for flexibility, so that when you need something a little bit faster or slower you do not need to switch to a different function.
I'd also update
examples/time.roc, which currently readsSleep.millis!(1000)with a# 1000 ms = 1 secondcomment.
Sounds good!
I'm interested in both once I've got a couple of the smaller ones through.
Feel free to try and see how it feels :)
Thanks @Anton! Opened as #460 — seconds! : F64 => {} over the existing Host.sleep_millis!, no host or glue changes, plus the examples/time.roc swap.
One thing worth a look that we didn't cover above: the edge cases. F64.round_to_u64 crashes on negative, NaN, infinite, and out-of-range values, which doesn't work for a signature with no error channel — so I went with F64.to_u64_try and split the rejected set by sign. Negative and NaN return immediately (matching Go's time.Sleep), and anything past U64.highest ms saturates there.
Measured against a local platform build: 1.5 → 1500 ms, 0.25 → 253 ms, -5 → 0 ms, NaN → 0 ms. Full rationale is in the PR description — happy to change the semantics if you'd prefer something else.
I don't think rounding should ever crash
if we can do it in 1 instruction and that 1 instruction works exactly the same way for all NaN values regardless of bit pattern, we should offer that
but if we're doing a conditional on it then we should just offer _try and that's it
Looked at the wiring: every round_to_* is out_of_range_or_crash(to_X_try(round(self))) — the crash is just the _try's Err(OutOfRange) re-wrapped as a crash. And to_X_wrap (total: NaN/±inf → 0, wraps on overflow) and to_X_try (fallible) already sit right next to it.
The raw hardware truncation isn't uniform across targets for the bad cases (x86 → integer-indefinite, ARM fcvtz* → saturates, wasm trunc → traps), so there's no free "1 instruction, same for every NaN" version — a total answer has to pick a policy behind a branch. By your own split that lands on: drop the crashing round_to_* in favor of round_to_*_try (→ Try(Int, [OutOfRange])), leaving _wrap as the explicit total escape hatch. Making round_to_* silently saturate would bake a clamp policy into a "total" signature, which feels less Roc than making the partiality explicit.
If that's the direction, happy to take it as a pure-Roc PR — swapping the out_of_range_or_crash wrapper for the Try across the round_to_* family + snapshots. Or would you want a saturating variant too?
no let's just do the _try ones
Before I open a PR: the crash isn't just round_to_* — floor_to_* and ceiling_to_* route through the same out_of_range_or_crash helper, so it's 90 fns (10 int types × Dec/F32/F64), not the ~30 I said. I've prototyped all three as <op>_to_<int>_try : Frac -> Try(Int, [OutOfRange]) (helper dropped; nice bonus — the out-of-range/NaN path is finally testable). Two calls for you: all three in one PR or split by op, and naming as round_to_i8_try (mirrors to_i8_try)?
one pr is fine :+1:
and that naming is fine too
Opened as https://github.com/roc-lang/roc/pull/10360 — all three families (round/floor/ceiling) → _try, one PR. Thanks for the quick calls!
Last updated: Aug 12 2026 at 12:35 UTC