This topic relates to #ideas > Calling style for special functions like `dbg` and `crash`
I often happens, that i want to do a dbg-statement, but only conditionally. For example, if I am deep inside a recursive function and printing the value on each step would be to much output. For example:
myfunc = \arg, deep ->
if deep == 0 then
arg
else
# dbg arg but only if deep%100 == 0
myfunc(doStuff(arg), deep - 1)
My current workaround looks like this:
myfunc = \arg, deep ->
if deep == 0 then
arg
else
_ =
if deep % 100 == 0 then
dbg arg
42
else
42
myfunc(doStuff(arg), deep - 1)
This is not nice. I don't want to return a value, but I have to. I don't want to have an else branch, but I have to. And I get a compiler warning for not using the value.
It would be nice, if dbg arg could work like an effectful function that returns {}. In the thread linked above, the term debugged was used.
So what I would like to work is:
myfunc = \arg, deep ->
if deep == 0 then
arg
else
if deep % 100 == 0 then
dbg arg
myfunc(doStuff(arg), deep - 1)
Maybe the same should be possible for crash and return.
Oh, that's an idea! We could have crash <expr>, dbg <expr>, and return <expr> all be statements that evaluate to an unbound type
And then during type unification, they would solve to the unit type (used to be {}, will soon be ())
So dbg <expr> returns an unbound type, but method .dbg() returns the value it was called on
I think we should also consider making crash a statement like return instead of a keyword in an expression context
It would mean that the only "special functions" we have are all statements that are "word + space + single expr"
And as you point out, this pairs very well with else-less if
I'm a fan of this
yeah I guess having a separate dbg keyword from .dbg() does simplify the types
I'm game! :+1:
regarding crash and return, I think as long as ?? crash ... and ?? return ... work, that sounds good
definitely foo(arg1, crash ...) doesn't make sense and the compiler should tell you it doesn't make sense, and making crash work like return seems like a good way to do that
after all, they both immediately exit the function :big_smile:
Richard Feldman said:
definitely
foo(arg1, crash ...)doesn't make sense and the compiler should tell you it doesn't make sense, and makingcrashwork likereturnseems like a good way to do that
I could see someone doing that in practice short term. fn(arg1, crash "Todo: figure out this arg")
Not saying we need to support it though...can just do:
arg2 = crash "Todo: figure out this arg"
fn(arg1, arg2)
I think it's best for the compiler to tell you not to do it, because otherwise you might be unpleasantly surprised at what happens if you do .with_default(crash ...) :sweat_smile:
Cause it isn't a lazy crash?
Cause the ... will crash before... crash crashes...
yeah since Roc is strictly evaluated, all arguments to functions get evaluated before the function does, so the fact that with_default only conditionally returns the value doesn't matter, because with_default never even gets called due to the crash happening first
Yep
In my mind that is the normal for essentially all languages, so it doesn't matter
with_default would have to take a lambda for it to work otherwise
So not really surprising.
Oh, I guess surprising with ??
But not with the raw function call
Cause ?? feels like it should be lazy.
Kinda like else unreachable or else panic in zig
yeah with ?? it works fine
because ?? is lazy
it desugars to a when, not to a function call
so both ?? crash ... and ?? return ... actually do work
because they're desugaring into putting the right hand side inside the desugared when's Err(_) -> branch
Oh cool. Them everything works as I would expect including using crash as a function arg.
but yeah I think if someone did write that by mistake, the compiler telling them about it would be helpful
Sure. Though, it would just be a warning, so I would expect the warning to pop up at the same time that I roc ... and hit the crash anyway.
I think if someone writes
email = user_emails.get_or(id, crash)
That should be a warning (but still run). They should need to write
email = user_emails.get(id) ??
crash "reason"
this shouldn't be a special case warning. it should just be an extension of the unreachable code checking to see that crash passed as an argument is unreachable.
@Ayaz Hafiz I'd go a step further and suggest that crash should be a keyword that can only be used as a special type of statement, not as an expression. And you'd get a runtime error that says "you can't use crash as an expression, it's for statements!" which would produce the desired effect of "crashing" early
This would fall out of crash being in the KEYWORDS list
im confused by this. what is the difference between a statement and expression? isn't foo ?? crash ... an expression including on the LHS and RHS of ???
before desugaring it would be an expression, yeah
why is it not an expression after?
I wasn't being pedantic enough, maybe
Code blocks that are on the same line as the preceding operator or whatnot are just expressions
But once they go to a newline, they become lists of statements
Which I believe is called a block
When you type foo ?? crash "message"
That would not be valid in the Roc I'm thinking of
why not? is that different from the warning you suggest above?
Since it represents
binop:
operator: "??"
left: var "foo"
right: function call:
function: "crash"
args: ["message"]
Or something like that
What I typed was
binop:
operator: "??"
left: var "foo"
right: block:
statements:
- crash statement:
message: "message"
Meaning that there is a type of "statement" called a crash statement
sorry, im confused on the semantic difference statements vs expressions provide
Stmt {
Crash(Loc<Expr>),
Dbg(Loc<Expr>),
Assignment(Loc<Pattern>, Loc<Expr>),
Plain(Loc<Expr>),
}
what is the purpose of that distinction though? Couldn't those all be expressions instead?
I'll try to think of a good explanation, one second
I think it helps us manage ambiguity in a Whitespace Significant Syntax
gotcha that makes sense during parsing
func = |left, right|
x = Stdin.line_with_prompt(left)!
y =
Stdin.line_with_prompt(right)!
Ok(x.concat(y))
Yeah, it's all for parsing
Is that second line_with_prompt! call supposed to be assigned to y?
it might be simpler to get rid of the distinction after parsing. im not sure this affords much after that. im not sure there is a good semantic difference between an expression and statement in the language like there is in most.
After parsing we get rid of the distinction for sure
It just becomes either Let { definitions: List<Definition>, result: Expr } or just Expr
Including Expr::Crash { message: Expr } and Expr::Return { result: Expr } for early returns
okay that makes sense to me
So I've been ignoring you type value ?? crash "message" because it'd need to be
value ??
crash "message"
Minor parsing thing
Same page
sorry now im confused again
why are those different
Because we parse expressions and statements differently
A statement can be { x, y } = EXPR
ok so its an implementation bug that they're different? or intentionally
But an expression can't start with an assignment, for one example
I'd say it's a feature that comes from preferring a simpler grammar
Though it could make our language more expressive to allow crash "message" as an expression,
It would mean that with the new style of PNC, there are random things that are space-delimited instead
Like maybe dbg and crash
does this not work? i feel like i have written this code before
when foo is
Ok x -> x
Err _ -> crash "unreachable"
That still works because we currently support two syntaxes at the cost of complexity
To put it bluntly, I want our language to become less expressive so that it's easier to parse, and to understand as a user how it's parsed
We currently allow parens AND spaces for function (and keyword) calls
But it'd be nice to say only PNC is used for everything
EXCEPT
sure, i get that and it seems prudent to remove that
For statements, which only have to say "check for these 3 keywords, otherwise do the same basic stuff"
i don't understand why
value ??
crash "message"
and
value ?? crash "message"
are different - it seems like a different in formatting. Like does
value ??
someOtherValue
not parse then?
The first one is a ?? binop expression where the left arg is a var, and the right arg is a block with one "crash statement"
(in my proposed Roc)
The second one is a ?? binop expression where the left arg is a var, and the right arg is an expression with a weird looking space-delimited function call
To a function called crash that takes a single Str for an arg
The third parses today and in the future as a ?? binop expression where the left arg is the value var and the right arg is a block with a single expression that is a plain someOtherValue var
And as an aside, the last plain statement in a block is what's returned from the block
let x = if value.is_ok() then value else {
Ok(someValue)
}
Is another way to conceive of that last one
i think i understand in principle the advantage of "block" so that you can easily interpret a sequence like
dbg foo
crash ""
1
that makes sense to me. But I don't follow why the indentation of crash should be semantically significant. Would it easier to make a block a list of expressions (no statement concept in the AST) and then crash is an expression you handle like everything else?
In WSS languages like Roc, indentation generally means the same as a brace block in a C-style language. It isn't crash, it's the indentation
The reason why I think crash should only be a statement and not an expression is so that the parser doesn't have to check for spaces after the first word of each statement
Fewer cases to handle
We can do it, but if we don't have to, then Roc is simpler
This is literally all a simplify parsing thing that affects nothing once we get to and get past canonicalization
My perception here is also aided by a familiarity with how our parser currently works
does it not need to check for spaces regardless for error tolerance? the thing that confuses me is that crash "" is semantically an expression (at least to my understanding) - it has a result value, unlike dbg "" or x = .... so it's hard for me to understand why crash is different
yeah i understand that this just might be implementation stuff
Where this is an Expr and this is a ValueDef, which is roughly a statement
When you say "semantically"
right, crash is an expression there
But in the future, I want it to only be part of ValueDef and not in Expr, until can::Expr
I think dbg should also not be in parse::Expr
if the concept of statements/ValueDef has "plain" expressions, what is the value of the difference?
Without getting too into the details of why, I'd say it helps us avoid wasting work in parsing
gothca
Go team
do you have a diff or something that shows the wasted work? just as a user i would find it confusing that i have to write
when x is
Ok y -> y
Err ->
crash ""
or in general force indentation without understanding this "blocks" concept, which is an implementation detail iiuc
Yeah, I guess it is an implementation detail
i get it for x = y because that has to be followed by a value and there is no way to have multple values on a single line in Roc but I don't get it for crash
I think that
when x is
Ok y -> y
Err _err -> crash "message"
Would be nice to allow
Same with return and dbg
I'm thinking about
func.call(arg1, func2(|arg3| arg3 + 1, crash "abc", arg4))
Roc used to be a very strongly whitespace-important language
But now it has two general camps: whitespace and brackets
And I think whitespace is still the right tool for demarking control flow
But parens are the right tool in modern Roc for demarking prodecure usage
So crash being "shaped" like a procedure is bad to me
right, in that func.call case it seems strange that i have to break it up into multiple lines and force indentation - i get it now with the blocks concept but that seems like a hard thing to learn. Like especially if I want to write crash "todo" while i figure out what to put there
Since it's reeeeally control flow in my eyes, being an early return of sorts
i don't think crash is control flow
It is and it isn't
It acts like a break
But with TNT
yeah but you don't really use it for control flow - like using for control flow would be exceptions, which this is not. I think in practice crash is saying "there is a value here", in the same way unwrap or assert is saying "this value exists, my logic is cannot be expressed in the type system". Or you treat it as a placeholder for a value
You're right that you don't use it as a tool that subverts control flow for the purpose of running your application
But you do use it to say "I want to fail early by pulling the plug as soon as we get here, since we should never be here in the first place"
yeah agreed
I want stuff that acts special to look special
i think it would be really unfortunate if
func.call(arg1, func2(|arg3| arg3 + 1, crash "abc", arg4))
was not legal. it just seems like a user burden
Function calls should look boring
Well, if I'm reading over code, I want to see the crashes obviously
That looks hidden to me
Especially in a safe language like Roc, you should never need it
So if you need it, it should be obvious that you're using it
Though if you don't agree with that principle, then I see your argument
We had a discussion on this in #ideas > Calling style for special functions like `dbg` and `crash`
if you never needed it the feature shouldn't exist - but it was introduced because there was a user need. maybe it should be removed then?
actually wait maybe here's a better example of what i'm getting at
my_crash = |s|
crash s
foo(my_crash(s))
foo(
crash s
)
it seems weird to me that one of these doesn't require indentation but the other does
Ayaz Hafiz said:
i think it would be really unfortunate if
func.call(arg1, func2(|arg3| arg3 + 1, crash "abc", arg4))was not legal. it just seems like a user burden
legal as in accepted by the parser?
(but still gives you a warning in a later phase)
yes, accepted by the parser
should definitely give a warning, but the warning should be semantic (determined by the typechecker) not syntactic, IMO
that makes sense to me
I think the same argument could be made for return
I mean, that's the beauty of purity inference, isn't it?
like "hey I understand what you're trying to do here, but this isn't the right place to do it"
as opposed to "I don't even understand what you're trying to do" (parsing error)
yeah makes sense for return too. or even standalone dbg
It's amazing to know when someone somewhere in the call stack is doing something effectful. I wish we had that for other stuff as well, but we already have ! for effectfulness
Something something algebraic effects...
But if we can get by with just PI, that'd be much simpler to read and learn
Last updated: Jun 16 2026 at 16:19 UTC