Our recent discussion on typechecking doc code blocks (#contributing > Ironing out doc code blocks) had a mention by Richard that he likes to put ... in teaching examples. It serves the purpose of allowing for obviously incomplete code used for explaining something in example code. An example usage would be:
update_file! = |path|
contents = File.read!(path)?
lines = contents.split_lines()
updated_lines = lines.keep_oks(|line|
updated = ... # your answer here
Ok(updated)
)
new_path =
if ... then ... else path
File.write(path, updated_lines.join("\n"))
Ok({})
This is a feature that sees success in Python. An ellipsis would function as a crash "not written yet" AST node, but unlike crash:
This is a minor, but nice feature on its own. I think it becomes more desirable when we look at some of the other places it comes up:
allow_todos annotation, we could allow ellipses and then ensure there are no errors or warnings.read_file! = |path: Str| = ...Some notes:
... to …, the Unicode character for an ellipsis. We don't currently do this anywhere in Roc, so unless we do it for other operators (maybe Unicode arrows)... a keyword rather than an operator since it's not used on anything, but is its own entityAlternative syntax, todo keyword:
update_file! = |path|
contents = File.read!(path)?
lines = contents.split_lines()
updated_lines = lines.keep_oks(|line|
updated = todo # your answer here
Ok(updated)
)
new_path =
if todo then todo else path
File.write(path, updated_lines.join("\n"))
Ok({})
Todo is nice but … or … helps a lot in those docs situations
But the dots are less visible in real code
As with other operator discussions, there is a benefit to having symbols over a word where symbols are less obvious in their purpose, meaning people assuming incorrectly less what they're meant to do. todo is pretty obvious when you want to implement something later, but it wouldn't work for
read_file! = |path: Str| => List U8
...
If we put todo, we'd be implying we wanted to implement the body later, but that's not true
Also, todo is currently a valid ident, but an ellipsis is not
So there's less clash with potential code
I like ..., it looks useful
this is interesting! It reminds me - I actually added a todo to elm-test back in the day. The way it worked was that it didn't clutter your output with warnings when you were running your tests and there were failures, but as soon as you got them all passing, instead of saying "test run succeeded" it would say "test run incomplete" and then tell you about the TODOs that were remaining now that you'd gotten everything else passing
so we could do something similar - make it so that ... doesn't trigger a warning, or an error, but rather a third thing
It being an entirely different class of thing is scary, but that's what it should be
Using warnings is just the best way to fit it into our current paradigm
yeah - quick note:
It will produce a warning itself that says "This code is not finished yet" or something like that, which doesn't block running the code, but it blocks deploying it to production
I've become generally skeptical of the idea of "this blocks deploying to production"
maybe another way to say it is that I've gotten increasingly convinced that "always inform, never block" is the way to go
a good example is nonzero exit codes for things like this
Okay, you've been pushing this, makes sense
We're planning on doing "always inform, never block" for roc build as well, then?
like yeah that's going to fail your CI, but if you decide for some reason you really need to ship this as-is to put out a production fire that's even worse than a potential crash (in maybe a code path that's behind a feature flag or something), we're not going to tell you you can't optimize it or whatever :big_smile:
Okay, fair
Then I think this still could be a warning
But a third thing is more appropriate
I think the "third thing" could take the form of combining all the TODOs into a single warning
although maybe that's not great for editors :thinking:
Per module, presumably?
well, editors want to be able to underline each location
and let you jump between them
I'm a little confused... why can't this just be a runtime error crash "no implementation"
and it would be confusing for the editor to get different warning counts from the CLI
would look like a bug
I understand. We'd need to be able to show a single message per module in the CLI, but multiple in the editor
That may be possible with not much work
Luke Boswell said:
I'm a little confused... why can't this just be a runtime error
crash "no implementation"
it can be, but then you can get spurious warnings about "unreachable code"
because you have code that occurs after the crash
and it's like "yes I know, I just haven't gotten to it yet!"
the other thing is that we'd want to filter these out when checking them for learning materials (e.g. tutorial)
because it's just fine to have a ... in a tutorial, no cause for concern
If it's a third thing, then that makes filtering them out super easy
yeah
well, probably also easy to filter out as a Problem variant
That was my plan
And we still have it punted, but once we get back to typechecking code examples, having ellipses would make me quite confident we should default to typechecking all code examples for peace of mind
yeah this seems worth trying out!
I'll make an issue tomorrow once more people get a chance to look this over
I have this hack in Rust I use, which is let todo = ();
which generates an unused warning which reminds me to come back to it later (I usually put a comment after it about what I wanted to remember to do)
this would be a much less hacky way to do that :stuck_out_tongue:
Why not use (try todo!)
well that doesn't give me a compiler warning :big_smile:
Oh yeah, it doesn't
the only annoying thing about it is that it spams me with warnings about all my let todo = (); until I get around to fixing them
hence the idea about "a third thing"
Sometimes being annoying is good
Squeaky wheel
this is not one of those times :laughing:
the reason I do it is because I want to remember to do it later
if the right time to do it was right now, I'd just do it right now :big_smile:
but I don't want to lose focus on the complicated thing I'm working on, and I also don't want to forget to do the cleanup thing or address the edge case later
and while I'm working on the complicated thing, I don't want to be bothered by the warnings about the thing I wanted to come back to later instead of thinking about now
but I can't think of an obvious way to have them show up in the editor and also not be annoying :thinking:
We need:
Love this idea! I'd probably use ... multiple times a day if it were supported in the languages I most work with at the moment.
Richard Feldman said:
but I can't think of an obvious way to have them show up in the editor and also not be annoying :thinking:
Could it be the same as with todo in elm-test? Don't show any output at all until you fixed all other problems, then when all regular warnings and errors are fixed the error list is populated with your "todo list"?
Maybe there'd be some way to actively get the todos even if you have warnings and errors, but it wouldn't be the default.
When I'm doing some complicated bit of work I keep a todo-list to jot down things I should definitely do later but not while I'm concentraing on one tricky part of the problem. It'd be amazing for Roc to have some facilities for that flow.
If we do end up with a 'todo'-style warning, I'd be super happy if there were a special comment that got tracked in it as well. Maybe # TODO, or if we don't want to add magic to that oft-used comment convention something else. Besides "I need to fill in this code later", another common item on my todo list is "this implementation kinda works, but definitely needs another pass before release".
I like the idea of ... but I'm trying to think where would that be used since we already have "type defs without implementations" as a thing.
I have used ... in the past tons of times when teaching people stuff or just doing kinda-fake code. But I wonder if I was using Roc I wouldn't be doing the same thing by just listing type defs + the thing that matters.
Just food for thought.
sum : Num, Num -> Num
multiply : Num, Num -> Num
calc : List Num -> Num
calc = | xs |
xs
.fold(1, multiply)
.fold(0, sum)
vs
sum : Num, Num -> Num
sum = ...
multiply : Num, Num -> Num
multiply = ...
calc : List Num -> Num
calc = | xs |
xs
.fold(1, multiply)
.fold(0, sum)
As a teaching tool, I'd say that this is pretty useful for inline omissions. You're right that we could do
find_answer = |x|
answer : U64
Ok(answer)
But that limits us to only omitting definitions, not arbitrary expressions
find_answer = |x|
answer = ...
answer2 = Str.repeat("abc", ...)
answer3 = if x then ... else ...
Ok(answer)
The ..., without knowing Roc, looks like a "we left this out"
And my hope is secondarily to make inline type annotations more achievable by letting us do
func = |arg1: Type, arg2: Type| -> Type
...
In the world with inline type annotations, we wouldn't be able to do that first thing with answer : U64, we'd have to do answer: U64 = ... or just answer = ...
Even without them, I think the ellipsis is more obvious with what should be added/replaced to a non-Roc dev
Your example convinced me on how they achieve different things. :grinning_face_with_smiling_eyes:
I often type crash "todo" as a placeholder. For example when working on different branches. It makes the LSP happy
Just for reference, Scala has something similar, though it’s ??? instead of ellipsis. Since question marks imply branches and early returns in Roc, ellipsis is probably better.
I love this idea! I shorten code samples with ... all the time, and it always feels a little incorrect to put something syntactically invalid in the code sample, and in some languages/renderers it breaks syntax highlighting.
So far we've described three different uses:
Would we want all three of them to be handled the same? Like, we've been talking about how this would warn when run. Would it be possible to implement it so it warns the right way when writing TODOs, doesn't warn if the function is defined externally, and still allows validation of docs?
If these three have any competing requirements, then we'd have to pick which of the cases to support.
@Sky Rose I'd say we're already doing that kind of thing for body-less annotations. We plan on showing a warning if it's not a host function, but no warning if it's for the host
With the discussion so far, I can't think of a feature I've proposed that I don't generally know how to implement already
But you raise a good question, if anyone thinks the three use cases might conflict someway, that'd be great to hear before we go ahead!
Empty function bodies, because the function is implemented as a host/builtin function. (This is useful for inline types because you wouldn't be able to write a bare type declaration.)
I would like to distinguish the two cases listed here:
I think that 2 should be .... I do not think 1 should use ...
Docs / examples / educational materials.
Sounds good, can just crash if run
TODOs while writing.
Sounds good, can just crash if run
The more I mull over magically-implemented functions (like host/builtin functions), the more I agree that ... shouldn't demark them. ... should mean one thing, which is currently "not done yet".
It'd be nice to get some extern keyword to make a function obviously implemented elsewhere like an FFI definition, as right now it's not visually obvious without doing platform dev why there are multiple functions that are just typed but not implemented. However:
Just a reminder of something I said earlier, Gleam did this when it only targeted Erlang/BEAM:
2824AB69-EACA-418F-9E84-8A13EE0E1DC0.jpg
And then uses this now that they target Erlang and JS:
BABA0E57-E3D6-489D-B870-C49EA9E4C144.jpg
We could do something closer to the former I think with maybe a “platform” keyword with the arg(s) being strings that the linker can understand
I'm open to it, but it would open the door to annotations on defs, which we've entirely avoided thus far.
So I'd rather wait to do it, but I definitely see the value!
Yeah, currently instead of having annotations on the defs, they have a special module.
At least for platforms
Builtins are magic with no indictation
I love this idea. Similar to expect, should these support doc comments so you can provide context when we hit the error at runtime?
when session is
LoggedIn user ->
user.name
Anonymous ->
## Waiting for design direction
...
I wonder if we could also print useful context, like:
when style is
Solid -> "solid"
_ -> ...
If we hit ..., we can print style with Inspect since that's the value being matched on.
Or even:
if is_active && upgraded.is_ok() then
perform_action!()
else
...
could print something like:
Hit ... at <file>:<line>.roc
is_active true
upgraded.is_ok() false
is_active && upgraded.is_ok() false
Agus Zubiaga said:
Or even:
if is_active && upgraded.is_ok() then perform_action! else ...could print something like:
Hit ... at <file>:<line>.rocis_active true upgraded.is_ok() false is_active && upgraded.is_ok() false
I love this!
Maybe with a stacktrace (maybe with a flag or env var)?
So you know what codepath caused that branch to be hit?
Yeah, like we do with crash
Yes, but hopefully not reusing the Rust env var ;-)
That backtrace is useful for ..., crash, and failed expects
It would be a good issue to implement afterwards for a generic "stacktrace" of values in scope
I feel like ... should just be sugar for crash. Any of these other features should be added to crash directly if wanted... Though ... could get a bit more power over the exact string it produces.
Well, an explicit crash means any code following is unreachable
But ... shouldn't have that property
But personally I would prefer for ... to only be sugar for crash "some sort of unimplemented message" and all the other features to be part of crash.
We also want ... to remind you for non-docs code that it's unfinished, which we don't want to do for crash
Yeah, those two sound fine
I mean all the fancier variable printing and back trace related stuff.
That feels generic
In theory, we can probably share a ton of the code for crash and ...
Yep, once you get to mono, they'll be the same thing IMO
Yeah
I'll write up a GitHub issue or two over the next hour
Seems like this has a pretty positive reception!
The warning theoretically could be generated during desugaring (though not sure it is actually wired up for that to work). That said, I'm not sure when we check for unreachable code. Probably could just modify crash nodes in the ast to have an extra variable that tells if it should generate the unreachable code warning. Then the lowering is 100% the same after desugaring
Would we actually want crash to print all that? It does seem really useful but couldn't that have a negative impact on performance for production code?
That's gonna be the first step. I've been assuming I'd implement this, but I think this would be a great issue for someone to learn about roc_can, so I'll write the issue assuming I'm not doing it
Maybe crash should only generate all that printing code in debug mode
crash is not considered recoverable at all, so I think more info is better
And slowdown at the end of your runtime isn't a problem IMO
Agus Zubiaga said:
Would we actually want
crashto print all that? It does seem really useful but couldn't that have a negative impact on performance for production code?
Personally I wouldn't want it any more with ... than I would want it with crash. Not sure if it should always be on or be a toggle though.
The suggestion of doing it only for debug builds would work
Yeah
Sam Mohr said:
And slowdown at the end of your runtime isn't a problem IMO
I think this would have an effect on performance even if you didn't hit the branch with .../crash because sometimes you'd have to increase the ref count of something to print it
Oh, you have a good point
crash is also wont to be used in higher-performance scenarios, I expect
Like *_unchecked functions
At least debug builds should get it
I think it would be used in high-perf scenarios so that you can unwrap things instead of returning tags
But --optimize builds should not get it, probably
Yeah, I think it should be fine if we only include all this info in debug binaries
whether it's crash or ...
In the CLI workflow design, I think there's gonna be separate flags:
--optimize means go faster--deploy-env=dev means include dbgs, and now crash "stacktraces"But by default, I'd expect dbg and all that to be omitted from optimized builds
So stack trace should exist in both cases (depends on debug info and the platform printing it). The bigger part is extra variable printing and such
Also, can we make a separate idea thread for that?
Yes
I think it is distinct from this work and exactly what would be included should be discuessed
https://github.com/roc-lang/roc/issues/7440
From the GH issue, my suggested CLI note is one of these per module:
── UNFINISHED CODE in path/To/Module.roc ───────────────────────────────────────
I still see a 3 places where code was left unfinished in this module:
35| if x == ... then
^^^
37| abc = ...
^^^
60| func.call("foo", ...)
^^^
────────────────────────────────────────────────────────────────────────────────
0 errors and 0 warnings found in XYZ ms.
I think adding notes to the last message AKA
0 errors, 0 warnings, and 1 note found in XYZ ms.
gets a little noisy, but that also would work for me
If we do that, we should probably not report notes unless we have them, meaning the message is now differently formatted if there's notes or not, but it makes it more readable
This discussion of adding comments to ... makes me wonder, should there be a todo keyword? Unlike crash it would allow execution to continue (if possible, maybe not always) and would warn that it's unfinished. Then, ... is sugar for todo "unimplemented", not crash.
when session is
LoggedIn user ->
user.name
Anonymous ->
todo "Waiting for design direction"
I think if ... and todo solve the same purpose, it'd be better to pick one to optimize for "one way to do things". I think todo is nice because it builds in a way to notate why the code is left unfinished, but it seems like people would rather have a terse way to do that without needing to write a message
... serves a slightly different purpose: being unobtrusive in docs and code samples. It can coincidentally behave the same as todo, but it would need to look different.
So if people were okay with forcing users to put a message, then todo "reason" is probably a good replacement for ..., otherwise we should just stick to ...
Hmm... we could make ... only work in code examples, and todo work everywhere else
I think ... should be a warning. Leaving it as a note and having an exit code of zero is going to pass CI. I also am weary of "notes" because there is nothing else in the language that currently needs to be a note, and if something is incomplete, the compiler should probably be telling you about that.
And then ... -> todo "unimplemented" would work for me
The GitHub issue says a non-zero exit code
I'm okay with warning, though.
Does crash not work for todo? You can always alias `todo = \x -> crash "todo: \(x)" if you want
crash leaves "unreachable code" warnings
It wouldn't be that hard for todo to desugar to Crash { kind: Todo, ... }
And only raise "unreachable code" warnings for Crash { kind: Crash, ... }
Couldn't ... just then consume the rest of the line and use that as a message? That text could then be formatted as an (optional) comment?
when session is
LoggedIn user ->
user.name
Anonymous ->
... Waiting for design direction
But highlighted like this (without the #):
when session is
LoggedIn user ->
user.name
Anonymous ->
... # Waiting for design direction
What if you have func.call(..., "abc")
Whomp whomp
Last updated: Jun 16 2026 at 16:19 UTC