I have discovered what appears to be a bug in compiling modules which require params. Not sure exactly what is causing it but:
I have a module which depends on two parameters. The code in this module is verified to be compilable/runnable by copy-pasting it directly into a roc application. The only modifications are to rename the functions imported through module params to their basic-cli names.
However, when running roc check
on the module, I get a compiler panic.
thread '<unnamed>' panicked at crates/compiler/can/src/pattern.rs:917:18:
internal error: entered unreachable code: Any other pattern should have given a parse error
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
There was an unrecoverable error in the Roc compiler. The `roc check` command can sometimes give a more helpful error report than other commands.
The module code is as follows (note that the only changes between the exported code and the code that works inside a roc app is that sendHttpReq
and getEnvVar
were replaced with Http.send
and Env.var
respectively.)
module {
sendHttpReq,
getEnvVar,
} -> [serperTool, serper]
import InternalTools
serperTool =
queryParam = {
name: "q",
type: "string",
description: "The search query to send to the serper.dev API",
required: Bool.true,
}
InternalTools.buildTool "serper" "Access to the serper.dev google search API" [queryParam]
serper : Str -> Task Str _
serper = \args ->
apiKey = getEnvVar! "SERPER_API_KEY"
request = {
method: Post,
headers: [{ key: "X-API-KEY", value: apiKey }],
url: "https://google.serper.dev/search",
mimeType: "application/json",
body: args |> Str.toUtf8,
timeout: NoTimeout,
}
when sendHttpReq request |> Task.result! is
Ok response ->
response.body
|> Str.fromUtf8
|> Result.withDefault "Failed to decode API response"
|> Task.ok
Err _ ->
"Failed to get response from serper.dev"
|> Task.ok
Interesting. I’ll take a look in a bit.
@Agus Zubiaga Looks like I jumped the gun here on assuming it has to do with module params. Seems that for some reason it doesn't like using my InternalTools import, even though I'm importing and using it in other modules.
Okay, definitely a weird bug here though - if I do import InternalTools exposing [buildTool]
and call buildTool
unqualified, it checks just fine.
I copied the header into a module and replaced the body of the functions with crash
, and I still run into it
Forgot to say, I removed the import InternalTools
line and it still failed
This seems to be enough to cause it:
module {
sendHttpReq,
getEnvVar,
} -> [serper]
serper = \args ->
crash "todo"
Wow... thats interesting. Thats pretty minimal.
I think we are not handling SpaceBefore
nodes in that specific function in can
I'm surprised about this because I just reused an existing one
it might expect those to be removed by an earlier pass, though
After getting roc check
to pass on the module (with unqualified buildTool), I imported the module to my app and got the following:
internal error: entered unreachable code:
No borrow signature for LambdaName { name: `54.IdentId(0)`, niche: Niche(Captures([])) } layout.
Tip 1: This can happen when you call a function with less arguments than it expects.
Like `Arg.list!` instead of `Arg.list! {}`.
Tip 2: `roc check yourfile.roc` can sometimes give you a helpful error.
Hm, that seems like a separate issue
Can you try removing the newlines in the params pattern?
module { sendHttpReq, getEnvVar } -> [serperTool, serper]
I think the first issue was only caused by that
I have tests for multiline params, but only for the parser which don't go through canonicalization
Ok yeah, I can confirm multiline module param patterns break in canonicalization because they don't go through desugaring like the other patterns do. I'm on it!
Agus Zubiaga said:
Can you try removing the newlines in the params pattern?
Yes, that is passing roc check
:+1:
but you still get that mysterious "borrow signature" error when you run, right?
Correct. I have a second module with different params that produces the identical error as well.
Ok, that one seems to be something going wrong in lowering, which won't be as easy of a fix. I'm probably going to need the whole project (as much as needed to reproduce) to find where it's going wrong.
If you can share that a branch, or a gist where this is reproducible, that'd be great!
Full code base is here: https://github.com/imclerran/roc-openrouter/tree/prefab-tools (prefab-tools branch)
awesome, thanks!
hangon, I'm gonna create a new static branch so I don't mess with anything you're looking at.
https://github.com/imclerran/roc-openrouter/tree/lowering-bug
Oh and this may be apparent, but its the examples/tools.roc file which can reproduce the bug.
I looked deeply into this issue today.
At first I thought it had to do with putting ai.PrefabTools.Serper.serper
(a function that uses params) inside a Dict
, but that seems to be handled properly by the lower params step. It correctly wraps the reference in a closure that captures the params so that it can be passed around. I tested doing something similar in a smaller example, and it worked just fine.
After that, I tried removing all params in the example and pass sendHttpReq
directly as an argument to Tools.handleToolCall
, and I found I still run into the panic! So this appears to be an unrelated compiler bug. I thought it was caused by passing sendHttpReq
as an argument/param, but even if we don't do this (and replace its call with a crash
), we run into a similar panic.
My guess is that there's a type error somewhere in the program that we aren't catching by the time we reach borrow inference. Unfortunately, there's a lot of code involved (including packages), and it's pretty hard to find the root cause. I think the next step should be to try to find the smallest example that can reproduce the issue.
Thanks for looking at this Agus. I'm working on breaking this down to a minimum reproduction. I've stripped it down a bit, but have a long way to go to be any where near a minimum reproduction.
One interesting finding so far is that moving the call to Tools.handleToolCalls
out of the loop eliminates the error.
Alright, I've got a min repro:
app [main] {
cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}
import Module { task: (Task.ok {}) }
main = Task.loop! {} loop
loop = \{} -> Task.ok (Step Module.effect!)
module { task } -> [effect]
effect = task
Issue filed at #7116
One additional finding:
if I inline the loop as an anonymous function, the issue does not occur.
IE the follwing does not cause the panic:
main = Task.loop! {} \{} -> Task.ok (Step Module.effect!)
Thank you so much for breaking this down, this is way more workable!
Interestingly, I was able to reproduce the panic on the original code without using params. However, at least in your example, it looks like the issue goes away if the task is passed as an argument, so there does seem to be an issue with params after all.
That is interesting... sounds like there may be two potential issues at play here? :thinking:
Yeah, maybe. I'll try to fix this and we'll see if that solves the issue in roc-openrouter
:smile:
I had forgotten you mentioned hitting the panic with sendHttpReq sent directly as an function argument... I guess if the panic was the same, its probably the same bug. Funny that passing the task as an argument to the function doesn't work in the min repro though...
Last updated: Jul 05 2025 at 12:14 UTC