Stream: contributing

Topic: basic-cli good first issues


view this post on Zulip Luke Boswell (Jul 22 2026 at 00:52):

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.

view this post on Zulip Dzmitry Misiuk (Jul 24 2026 at 15:31):

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.

view this post on Zulip Anton (Jul 24 2026 at 15:40):

F64 for fractional sleeps like 1.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.

view this post on Zulip Anton (Jul 24 2026 at 15:41):

I'd also update examples/time.roc, which currently reads Sleep.millis!(1000) with a # 1000 ms = 1 second comment.

Sounds good!

view this post on Zulip Anton (Jul 24 2026 at 15:41):

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 :)

view this post on Zulip Dzmitry Misiuk (Jul 24 2026 at 16:06):

Thanks @Anton! Opened as #460seconds! : 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.

view this post on Zulip Richard Feldman (Jul 24 2026 at 17:21):

I don't think rounding should ever crash

view this post on Zulip Richard Feldman (Jul 24 2026 at 17:22):

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

view this post on Zulip Richard Feldman (Jul 24 2026 at 17:22):

but if we're doing a conditional on it then we should just offer _try and that's it

view this post on Zulip Dzmitry Misiuk (Jul 24 2026 at 18:11):

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?

view this post on Zulip Richard Feldman (Jul 24 2026 at 18:35):

no let's just do the _try ones

view this post on Zulip Dzmitry Misiuk (Jul 24 2026 at 20:12):

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)?

view this post on Zulip Richard Feldman (Jul 24 2026 at 20:51):

one pr is fine :+1:

view this post on Zulip Richard Feldman (Jul 24 2026 at 20:52):

and that naming is fine too

view this post on Zulip Dzmitry Misiuk (Jul 24 2026 at 21:22):

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