With the move to parentheses-based function calls (let's say PNC), we now have a potential schism between how we call user-defined functions and our special keywords: dbg, crash, try. It's likely that try gets removed once PNC is ready, but the other 2 will still be around. They are not currently visually distinct from function calls (unless syntax highlighting is aware of them), but it would be nice to know when your special keywords (dbg logs to the console) were visually distinct for scanning code.
We have a few options as to how we can call them, given these type signatures:
dbg : a -> a where a.to_str() -> Str
crash : Str -> a # an unbound variable
dbg "abc"
func.call(123, dbg)
Maintain the current syntax, but allow for passing them around as functions.
dbg("abc")
func.call(123, dbg)
@dbg("abc")
func.call(123, @dbg)
dbg!("abc")
func.call(123, dbg!)
Of these options, I'd vote for @ prefixes since they're unique in Roc and don't confuse users about !, but are visually distinct. Close second is using parens and relying on syntax highlighting. Thoughts?
I see no reason to make them special. I would vote for just treating them like normal functions
keep in mind return doesn't do this
and it doesn't in mainstream languages either
Yep
so there's precedent both ways
same with stuff like throw or raise
Roc has been good at annotating control flow subversion with purity inference, however
You're right that this would be exceptional
Personally, they just feel like normal functions to me. So I really don't feel there is any need to make them stick out more than a slightly different syntax coloring. They should be usable where a lambda is usable.
Oh, just realized that today crash and dbg can't be used as lambdas and passed into other functions
In my opinion, if we allow them to be passed into other functions like lambdas (which I think makes sense), they should just be normal functions. If instead they are not allowed to be passed into other functions, it makes sense to keep them extra special
yeah I don't think they should be passed into other functions :big_smile:
I mean they are already trivial to pass into other functions \x -> crash "unreachable"
And I could see someone using dbg for a logger function in a simple platform.
So might pass it into a module param for a logger in another module
dbg is weird because it's currently either a statement or an expression. We could probably make it just an expr of type a -> a where a.to_str() -> Str but then we'd need to treat it specially for dbg(x) statements, since pure statements need to return {} and this returns x.
So at least dbg can't just be a normal function all the time, even if it is most of the time
Fair enough
I think that means it makes most sense to leave these fully special like return
It'd be nice to remove whitespace calling within expressions, but we have that for math operators
Like func.call(123, crash "an error occurred") would be nice to deprecate in favor of func.call(123, crash("an error occurred"))
Then assuming there's no prefix/suffix for these special functions, I'd vote for calling them like normal functions without spaces
Since it removes all whitespace delimiting from expressions
I'd like dbg to have the signature a -> a where a.to-str: (a -> Str) -> a
I love that Elm debug functions the passed in value
Sam Mohr said:
Like
func.call(123, crash "an error occurred")would be nice to deprecate in favor offunc.call(123, crash("an error occurred"))
in general I'd actually like to give a warning for using crash as an expression like this
That'd be nice
So much so that I implemented it in the formula language we use dynamic configuration of components in the product I work on at work
Anthony Bullard said:
a -> a where a.to-str: (a -> Str) -> a
dbg has two signatures a -> a where a.to-str: (a -> Str) -> a and a -> a where a.to-str: (a -> Str) -> {}
it's reasonable to use crash as an expression being given to a short-circuiting operator
but foo(bar, baz, crash ...) never makes sense
because it's just going to crash, so you might as well delete the foo(bar, baz part
it's reasonable to use
crashas an expression being given to a short-circuiting operator
We could still suggest using a if ... then in those cases.
Yeah, so if we keep that our statements are:
?
Brendan Hansknecht said:
Anthony Bullard said:
a -> a where a.to-str: (a -> Str) -> adbg has two signatures
a -> a where a.to-str: (a -> Str) -> aanda -> a where a.to-str: (a -> Str) -> {}
and worth noting that in Elm, the equivalent of dbg only has the expression version, and it leads to people doing things like _ = dbg ... all the time
Yeah, so if we keep that our statements are:
and dbg
I wonder if you could just have a UnitIgnoreExpr statement?
That's just an Expr that's followed by other statements, and in can we find out if the expr has the type {}?
Sorry for using the name Unit but that's how my functional brain thinks of that type
I presumed the long term plan for parsing output is that a module is:
function bodies are just lists of type or value statements as well
I would hate it if we enforced a separation of types and values long-term
it would be
pub enum Statement {
Alias(Pattern, Type),
Import(Module, Vec<Exposes>),
ImportFile(Path, Type),
Assignment(Pattern, Expression),
Return(Expression),
Dbg(Expression),
Bare(Expression),
}
pub struct Module {
header: Header,
defs: Vec<Statement>,
}
pub enum Expression {
Function {
args: Vec<Args>,
body: Vec<Statement>,
},
Value ...
}
And then canonicalization would block Dbg and Return outside of functions
What about annotated defs?
Is that just an Alias followed by a Bare?
You either during parsing combine Alias followed by Bare without anything between (like comments or newlines) into a AnnotatedAssignment
Or you do that in can
Yeah, that's reasonble
All that said, it means that generic "statements" are like 99% of a module
I'd probably collapse Return and Dbg into a single case where the first item would be a enum of {Return, Dbg} but yeah, this is good
And we'd also have to enforce imports at the top
Unless we want to parse the entire file to find dependencies
I don't think so, but I'd be okay with that
I'd be ok with it if we get local imports and import destructures :wink:
But I don't think that works with ImportFile
Unless it's replaced with a binding of a local variable to a module-level constant
I'm working on the can rewrite, and it entails us first compiling modules in silos for maximum cache friendliness. We need to go find all imports even within blocks anyway for that to do its job, so I don't think it's a problem if we allow nested (file) imports
It seems like @Anthony Bullard took away that we were gonna do dbg func(arg1, arg2), but I thought the takeaway was that we'd do dbg(func(arg1, arg2)). Which are we going for? The latter seems more consistent
And I presume Anthony and Josh would agree that it makes parsing simpler in addition
I thought it was the PNC version too
We are making crash a function, so only dbg is left as a "function" that uses whitespace. return uses whitespace, but it's different since it can only start an expression
Is there like a statement version that uses whitespace, and an inline expression version that is a function and just passes the value through?
I would argue at this point that they are clearly keywords/intrinsics and not functions
As such, none of the should use function calling syntax
Instead they should use the space calling syntax and be distinct.
So crash "my str" return abc and dbg some_fn(123)
A few points against that:
val.dbg()crash use parensBut I'm okay with it so long as crash and dbg act the same
hm, why are we having crash use parens? :face_with_raised_eyebrow:
Maybe I misread the code
I agree with Brendan, I think all of these should be like return or try or catch or throw in other languages
as in, no parens
For me, the main issue is that they don't fully act like functions. You have to wrap them in a lambda to pass them into another function.
I just wish they were parsed solely as expressions
I think it's ok if we have both dbg foo and foo.dbg()
That would simplify the grammar a lot
:thinking: although...if we have static dispatch, is there a reason we couldn't just always make it be foo.dbg()?
What about for non-nominal types?
we infer it
That would REALLY lead people to Nominal-land
just like we do for .equals
Ah
yeah we already infer a ton of things :big_smile:
.inspect too
Man, I just don't love the magical intrinsic methods
But i'm just one human
well I think it's essential that { ... } == { ... } works
among other things
and inferred methods seems like the obvious way to achieve that, especially if we want nominal types to be able to customize how == works on them
and it's not just equality, but also hashing (e.g. wanting to be able to use structural records, tags, and tuples as keys in dictionaries and sets)
and encoding/decoding
I guess that I don't like that now I have to know "if i see .equals() it is a compiler inferred implementation.....or maybe not...."
:thinking: what's the difference between that and any other method?
like anytime you see .equals() you have to know the type of the thing before the . to know where its implementation lives
and if you know its type is nominal, then you know it's in a module somewhere, but if you know it's structural, then you know it has to be compiler-inferred because there's no module where arbitrary structural records are defined :big_smile:
But if it's compiler derived normally, I might just assume there is no custom implementation
I see
I think that would only be true of dbg
And equals
oh equals you can for sure override!
so that you can customize how equality works e.g. for a user-defined collection like a tree data structure where you don't want equality to care about insertion order
I vote for only having .dbg()— that works everywhere.
The only caveat is that we have to allow it as a statement even if the value is not {}/().
So you can do:
…
result.dbg()
…
Currently that’s only allowed if the function is effectful
I mean, it’s allowed, but it triggers a warning
Presumably, we can desugar that to dbg(expr), and for top-level pure stmts check if it's an Apply(Dbg, ...) and not emit the warning. Should be simple to support
I like the idea of only having one way to do things, but in this case dbg expr seems like it reads forward a lot better
So it would be a downgrade to always read the dbg after the expression we're debugging
I think the warning should only be if the return type is NOT Unit
And then problem solved
Not quite for dbg
That's true for everything else
Or can’t we have an exception if the func is Dbg?
That's the idea, yep
dbg returns a?
Yep, so you can do func(dbg arg)
I think right now I’ve worked on the compiler for more hours than I’ve written Roc code :cry:
I'm DEFINITELY in that boat
It's a living
You can work on libraries any time, we need plenty of those
It would be a two step check for whether a pure statement is allowed:
Apply(Dbg, ...){}Yeah, so for the purposes of the warning, we would treat dbg as effectful
because it kind of is
But only at the top-level, I presume. Since Str.repeat("abc", 3.dbg()) would be doing work outside of the thing it debug printed
Yeah, but that’s the same rule for effectful fns
I don't think I agree. I thought that:
But func(arg1, val.dbg()) wouldn't work like 3 because the func(arg1, part isn't "debugful"
I'm sure I'm missing something here
Not sure how important it is for you to explain it to me as long as the implementation works :smile:
Personally I prefer dbg to generally be at the beginning of the line so I don't have to see:
"some really long message that feels out of place with the quote at the beginning of the line: ${var.to_str()}".dbg()
Same
It's why we at least need dbg expr if not also expr.dbg()
oh, I didn’t mean we would actually consider dbg effectful like in the type system, but only for the purposes of the discarded result in statement warning where I think would work the same.
I agree with that, I just think that you can't make all dbg calls as effectful for that to work since only the top one should get that tracking
Since I don't think we should allow Str.repeat("abc", 3.dbg()) as a pure statement unless the top-level value was debugged
Str.repeat("abc”, get_count!()) is effectful but would still produce the warning
Because you’re discarding the result of repeat
Oh, great!
So I think it’s the same
Didn't know that was the case
If that's the case, then we're good
Brendan Hansknecht said:
Personally I prefer
dbgto generally be at the beginning of the line so I don't have to see:"some really long message that feels out of place with the quote at the beginning of the line: ${var.to_str()}".dbg()
I dunno, I'm kinda skeptical that this would come up a lot in practice :sweat_smile:
I'd like to try it as just the method style and see how it goes
because we know we want that style so it can be used in pipelines
and maybe we want both, but let's see how much demand there is in practice if we have only the one way to do it
What should the precedence of dbg be?
In particular, how should dbg 4 / 2 be parsed?
That's currently (dbg 4) / 2, but if dbg is a keyword like return, I feel it should be parsed like dbg (4/2).
I ask, because I'm troubleshooting a bug the fuzzer found, where this:
dbg(a/a)
d
is formatted as this:
dbg a / a
d
... which of course is parsed differently.
I can have the formatter add parens like this fairly easily:
dbg (a / a)
d
... but when I read the above thread, it occurred to me that we may want to change the precedence of dbg anyway.
Secret option: we parse dbg in two ways
As the start of a statement, or as a method call
Then the precedence question goes away
I guess for now we can't do that yet, so we can maybe just treat dbg as a normal function call for simplicity
We absolutely can do that now. (unless I'm missing something?)
And that's roughly the direction I'm thinking here
I don't think we should focus on keeping it a space call even within expressions at this time
Hmm, can you more explicitly state what you think we _should_ focus on?
Sure
(or just, what you think we should do?)
My goal is to subvert complexity arising from an intermediate state that we don't care about
It seems like this question is effort we don't need to put in
Unless you really think so
My vote would be:
Copy the return <expr> parser
Make a dbg one
And otherwise allow treating dbg like a function for now
With parens
I think we're way more on the same page than you think we are
Oh, great
I'm distracted with choir stuff right now, I'll stop trying to multitask
Haha np
Richard Feldman said:
Brendan Hansknecht said:
Personally I prefer
dbgto generally be at the beginning of the line so I don't have to see:"some really long message that feels out of place with the quote at the beginning of the line: ${var.to_str()}".dbg()
I dunno, I'm kinda skeptical that this would come up a lot in practice :sweat_smile:
I do this fairly often when working on roc code. Sometimes I need more context than just the variable. Or want to print multiple variables in a single dbg statement.
Last updated: Jun 16 2026 at 16:19 UTC