so awhile back we changed Stdin.line
to produce [Input Str, End]
this makes the function a lot less helpful for learning, because this doesn't work:
main =
Stdout.line! "Type something and press Enter."
input = Stdin.line!
Stdout.line! "You entered: $(input)"
I wonder if we should revisit the original problem and reonsider the API
specifically, I'm wondering if we should solve it with two functions
Stdin.line
goes back to producing a Str
and then we have a different Stdin
function for reading until EOT
thoughts?
as a concrete design idea:
## Reads bytes from stdin until a '\n'
## byte is encountered, then parses
## all the bytes before it as utf8
Stdin.line : Task Str *
## Reads bytes from stdin until an EOT
## byte is encountered, then parses
## all the bytes before it as utf8
Stdin.eot : Task Str *
That sounds great!
I don't think that works. You often have to read in small chunks (like lines), but you also need to know when the stdin stream ends
I think you fundamentally just want the end of the stream to be seen as exceptional and ignore in the normal case
ok so then we could rename the current function to like Stdin.lineOrEot
or something?
and then reintroduce the previous Stdin.line
design
So I would propose:
Stdin.line Str [StreamEnd]
So you can do either;
# dont care
In = Stdin.line!
Stdout.line! "You entered: $(in)"
# do care
InRes = Stdin.line |> Task.result!
when inRes is
...
Probably with a better error message, but that general idea
to me, the main motivation of this change is to have Task Str *
as a beginner learning tool
I don't think we want to have to teach error handling that early
or Result
I strongly think Stdin.line
should do the right thing and have an error type. (Or just return an union directly)
We should be explicit if we make a version that crashes or just keeps returning empty input. That version should have the longer name
Stdin.lineUnchecked: ...
or etc
Also, you can hide the error handling by making the platform handle it by default
Allow any error to be returned to the platform
It can print the error (will be a low quality message) and crash
I think that would be a better way to delay introducing error handling
Stdout.line also should really return an error. Currently I think every call to stdout in basic CLI has 2 different ways to panic in rust. That is bad ux. Random crashes that the roc developer can't control.
We don't have some form of try catch for platform panics. So any errors not given to roc are either fatal or silent without any way to change that from within roc.
:thinking: why would that be true of Stdin.line
but not Stdout.line
?
(which can also fail)
It's true for both
I think both should return errors to roc
!
lets you easily ignore them for the most part
So the main code will still read just like a imperative program
Just will accumulate the errors.
ok so what does hello world in Roc look like in that world?
main =
Stdout.line! "Type something and press Enter."
input = Stdin.line!
Stdout.line! "You entered: $(input)"
that's a type mismatch because the errors aren't handled
Nope...one sec
mainForHost : Task {} [] as Fx
mainForHost =
res = main |> Task.result!
when res is
Ok {} -> Task.ok {}
Err e ->
# This could call an unsafe stderr and exit, or just use crash, or etc
crash "Program exited with error: $(Inspect.toStr e)"
So main can return any sort of error to basic cli and it will print out an crash.
Of course, main can also choose to send no errors. In that case, it can handle there errors in a nicer way
Now everything in main has the choice to use !
and never think about error handling if wanted
Then we can have:
Stdout.line: Str -> Task {} [StdoutClosed]
Stderr.line: Str -> Task {} [StderrClosed]
Stdin.line: Task Str [StdinClosed, StdinEndOfStream]
And the end user can just write your original program with !
.
Instead of maybe getting some random rust panic, They would get a roc crash (we can pick the exact behaviour) with the message: Program exited with error: StdinEndOfStream
.
ahh right, I remember this design!
we talked about it once, but we didn't have Inspect
back then
so now it seems more viable :+1:
and then if you want to guarantee you've handled all the errors in your application, all you have to do is to add this type annotation to your main
:
main : Task {} []
and then you'll get a type mismatch if any are unhandled
but if you leave off the annotation (e.g. in tutorials) or annotate it as main : Task {} _
then it will work
hm, but in that design how do you specify exit code? :thinking:
personally, I would pull the I32
out of the api and require calling Program.exit exitCode
or something during the error handling.
If you use the default handling, it could just exit with an exit code of 1
That said, totally could still leave the exit code in if wanted. I just removed it as personal preference.
mainForHost : Task I32 [] as Fx
mainForHost =
res = main |> Task.result!
when res is
Ok exitCode -> Task.ok exitCode
Err e ->
Stderr.lineUnchecked! "Program exited with error: $(Inspect.toStr e)"
Task.ok 1
actually something we could do is have the platform accept this:
Task {} [Exit I32]*
That is much nicer
so if you return Task.err (Exit 1)
then it exits with that code (just like today) but all other errors get handled in the default way
this design also makes basic-cli nicer for scripting in general!
means by default you get the "bail out on error" behavior, but you can easily opt into any amount of granular error handling and guarantees you like
I like not having exit
as a Task
because it affects control flow in a way that's otherwise reserved for crash
I can't name a concrete downside of that, but it feels like an invariant that shouldn't be broken lightly
so I like leaving it out of the API if we can get away with it :big_smile:
ok I'm game to try this design out!
This sounds perfect for basic-cli!
Let's do it
I should have some time today to work in this change for basic-cli and to update the examples.
Ah, inconvenient place for this module imports bug to appear... I might have to hack something to get it to work.
── MODULE NOT IMPORTED in examples/../platform/main.roc ────────────────────────
The `Stderr` module is not imported:
33│ Stderr.line! "Program exited with error: $(Inspect.toStr e)"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Did you mean to import it?
────────────────────────────────────────────────────────────────────────────────
1 error and 2 warnings found in 23 ms
Maybe can manually wrap/call the effect?
Will have to be different from the regular Stderr.line
anyway since you want to avoid errors.
And the new Stderr.line
will have an error case [StderrClosed]
Yeah, been playing with that... this bug is a bit nebulous and is proving hard to work around. I'm leaning towards just leaving the "print error" part out for a few days and we can add it back when Agus' PR lands which resolves this specific issue I think.
yeah can just use crash
for now
The other wrinkle, is that to use !
we need to have Task
imported in the app.
But if it's just main = Stdout.line! "Hello, World!"
we get **UNUSED IMPORT**: Nothing from **Task** is used in this module
which is a different incantation of the same bug.
Well, at least that this only a Warning so it still runs
But other than a stray Warning, it's working ok.
I'll update the examples.
We can clean it up later to have the nicer Inspect
printing.
oh that's funny - it's actually not a bug
Well, I guess we need builtin Task before we can resolve that
in the sense that if the only uses of !
are trailing (which desugar to nothing) then after desugaring there really is no use of Task in the module
yeah task as builtin takes care of that nicely
For the short term (like next 24hours) experience, users may get an unused warning in the specific case where they dont use anything from Task in thier app.
yeah which seems fine
https://github.com/roc-lang/basic-cli/pull/184
^^ PR for the basic-cli changes... will require tomorrow's nightly to pass CI.
Looks like there may be another issue too, so I'll plug away at it until I'm reasonably confident it's just waiting on nightly for Anton
I'll also have a crack at our https://github.com/roc-lang/examples
Wait, so is !
already released?
yeah, you can use it right now!
the benefits of all-nightly releases :grinning_face_with_smiling_eyes:
no need to wait for a "stable" release if we don't have them :laughing:
drew said:
Wait, so is
!
already released?
I updated the roc-wasm4
examples... it looks awesome with !
suffix :hearts: https://github.com/lukewilliamboswell/roc-wasm4/blob/main/examples/rocci-bird.roc#L36
The other platform that will be nice boost with the !
syntax is roc-ray for drawing graphics etc. Looking forward to upgrading to zig-12 with that too.
And I should probably make an actual release for macos/linux so people can use from just a URL. I was thinking it might be possible to do that using GH Actions somehow... I've never looked at that before, but it's just running zig build
on the different arch's and then packaging up the .a
files with the roc platform files.
also fwiw strong agree that Stdin.line
shouldn’t return a tagged type. the benefits don’t outweigh the costs imo. i was very confused when i randomly saw this API two days ago.
Yeah, I just changed it in
https://github.com/roc-lang/basic-cli/pull/184
to Stdin.line : Task Str [End]
instead of just End
, we should probably make a type alias for StdinErr : [IoErr, Oom, End]
(we can add more later)
how are task errors handled?
and have it be Task Str [StdinErr StdinErr]
can you simply ignore them?
essentially yeah
but can opt in to handling, i suppose, and then it is exhastive
right, give or take the OS introducing new types of errors that Roc doesn't know about yet :laughing:
nice
seems like a good compromise
Made an issue https://github.com/roc-lang/basic-cli/issues/185 to track this.
speaking of updates, while working on the tutorial I noticed something awkward: if Stderr.line
can error, then gracefully handling errors (e.g. by printing a message to stderr and then doing an Exit
) becomes pretty cumbersome because you have to do like Stderr.line "Error writing to stdout" |> Task.onErr! \_ -> Task.ok {}
what if instead we made it be Exit I32 Str
?
and then the platform prints that Str
to stderr, while automatically disregarding any errors ("if stderr is broken, you just don't get any output, sorry")
that's also nice in that it makes the errors even more concise to handle, because you can just say:
Exit 1 "Error writing to stdout"
instead of needing two lines, one of which is always a Stderr.line!
also makes it harder to make the mistake of using stdout (by force of habit) instead of stderr for error reporting :big_smile:
That sounds nice!
I wonder if it would be confusing that if you do Exit 0 "success!"
the message would print to stderr. The message could print to stdout if the exit code is 0, but that might encourage people to use the tag instead of just using Stdout.line
.
yeah that's probably fine
doing stdout for 0 makes sense to me :thumbs_up:
I guess a counterargument would be that it's a weird edge case you could trip over, and maybe you'd want to use Exit 0 "Warning: ..."
if you were translating an error into a warning and didn't want to change the exit code
I think it should always print to stderr with Exit
stderr tends to also be used for status updates and terminal outputs for pipelined applications
I'm not really following, Stderr.line
cant throw an error right now. Are we wanting to update the rust impl to return an error somehow?
yeah. eprintln!
just crashes on errror
I guess we could handle this
Panics if writing to io::stderr fails.
Writing to non-blocking stderr can cause an error, which will lead this macro to panic.
let stderr = io::stderr();
let mut handle = stderr.lock();
let result = handle.write_all(b"hello world");
yeah exactly :100:
in general, writing to a file descriptor can fail in various ways
and writing to stdout and to stderr is just writing data to a file descriptor
not all of those possible failure modes apply to stdout and stderr, but plenty of them do!
so in this new design, I think we should have Stdin.line
, Stdout.line
, and Stderr.line
all have errors
Also, lets please make sure to either make them wrapping or descriptive of the filed descriptor they come from. End
alone is quite unclear. Stderr End
is much much more useful.
I'd like to try out the convention of naming those errors Err
and just always fully qualifying them:
Stdout.line : Str -> Task {} [StdoutErr Stdout.Err]
Stderr.line : Str -> Task {} [StderrErr Stderr.Err]
Stdin.line : Task Str [StdinErr Stdin.Err]
yeah I think End
would be part of the Stdin.Err
tag union in this design
Sounds great!
Though I guess it only works if everything in Stdin
has the exact same Err
type.
yeah I can't think of any cases where that wouldn't be true! :big_smile:
I thought file would be a case, but from discussion elsewhere, I guess everything is just io error there
Do we have any ideas around the specific errors here? Rust just has a big std::io::ErrorKind
with lots of variants
So the updates I had been making were trending towards using that everywhere... but in the above API it looks like we want to tailor the tags for the specific function, and then maybe have an Other to catch the rest
yeah I think for now we can just put in a couple of them and add more later
the main thing is just switching over to the new "shape" if that makes sense
Also, I would like to use an <u8>
for the error tag between rust and roc instead of generating glue for these if that's ok
It's just much simpler. Regenerating glue is very handraulic at the moment because it doesn't generate valid Rust and I have to manually fix things
So stderrLine : Str -> Effect (Result {} U8)
instead of stderrLine : Str -> Effect (Result {} InternalStderrError)
as in just on the platform <-> host side?
not visible to application authors
correct
sure, seems reasonable! :thumbs_up:
Actually I'm going to use a RocStr, so I can return a description of the error from Rust
Last updated: Jul 06 2025 at 12:14 UTC